Files
SabreTools/SabreTools.Library/DatFiles/DatFile.cs
Matt Nadareski d379ef59ab [FIleTypes/] Migrate to individual input/output types
Similar to the migration of splitting DatFile into ifferent subtypes, this makes sure that logic that petains to each "type" of file that's used by SabreTools, be it an input/output archive format or a specialty file format that is treated by itself like CHDs, is in tis own namespace. ArchiveTools has been pared down accordingly and all "factory" logic should make it easier to add more formats in the future with little fuss.
2017-11-02 00:29:20 -07:00

6128 lines
178 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using SabreTools.Library.Data;
using SabreTools.Library.FileTypes;
using SabreTools.Library.Items;
using SabreTools.Library.Skippers;
using SabreTools.Library.Tools;
#if MONO
using System.IO;
#else
using Alphaleonis.Win32.Filesystem;
using FileStream = System.IO.FileStream;
using IOException = System.IO.IOException;
using MemoryStream = System.IO.MemoryStream;
using SearchOption = System.IO.SearchOption;
using SeekOrigin = System.IO.SeekOrigin;
using Stream = System.IO.Stream;
using StreamWriter = System.IO.StreamWriter;
#endif
using NaturalSort;
namespace SabreTools.Library.DatFiles
{
/// <summary>
/// Represents a format-agnostic DAT
/// </summary>
/// <remarks>
/// TODO: Make stats output standard width (HTML, without making the entire thing a table)
/// TODO: Stats multithreading? Either StringBuilder or locking
/// </remarks>
public partial class DatFile
{
#region Private instance variables
// Internal DatHeader values
internal DatHeader _datHeader = new DatHeader();
// DatItems dictionary
internal SortedDictionary<string, List<DatItem>> _items = new SortedDictionary<string, List<DatItem>>();
internal SortedBy _sortedBy;
// Internal statistical data
internal DatStats _datStats = new DatStats();
#endregion
#region Publicly facing variables
// Data common to most DAT types
public string FileName
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.FileName;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.FileName = value;
}
}
public string Name
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Name;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Name = value;
}
}
public string Description
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Description;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Description = value;
}
}
public string RootDir
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.RootDir;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.RootDir = value;
}
}
public string Category
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Category;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Category = value;
}
}
public string Version
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Version;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Version = value;
}
}
public string Date
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Date;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Date = value;
}
}
public string Author
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Author;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Author = value;
}
}
public string Email
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Email;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Email = value;
}
}
public string Homepage
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Homepage;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Homepage = value;
}
}
public string Url
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Url;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Url = value;
}
}
public string Comment
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Comment;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Comment = value;
}
}
public string Header
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Header;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Header = value;
}
}
public string Type // Generally only used for SuperDAT
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Type;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Type = value;
}
}
public ForceMerging ForceMerging
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.ForceMerging;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.ForceMerging = value;
}
}
public ForceNodump ForceNodump
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.ForceNodump;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.ForceNodump = value;
}
}
public ForcePacking ForcePacking
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.ForcePacking;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.ForcePacking = value;
}
}
public DatFormat DatFormat
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.DatFormat;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.DatFormat = value;
}
}
public bool ExcludeOf
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.ExcludeOf;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.ExcludeOf = value;
}
}
public bool SceneDateStrip
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.SceneDateStrip;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.SceneDateStrip = value;
}
}
public DedupeType DedupeRoms
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.DedupeRoms;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.DedupeRoms = value;
}
}
public Hash StripHash
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.StripHash;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.StripHash = value;
}
}
public bool OneGameOneRegion
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.OneGameOneRegion;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.OneGameOneRegion = value;
}
}
public List<string> Regions
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Regions;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Regions = value;
}
}
public SortedBy SortedBy
{
get { return _sortedBy; }
}
// Data specific to the Miss DAT type
public bool UseGame
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.UseGame;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.UseGame = value;
}
}
public string Prefix
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Prefix;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Prefix = value;
}
}
public string Postfix
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Postfix;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Postfix = value;
}
}
public bool Quotes
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Quotes;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Quotes = value;
}
}
public string RepExt
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.RepExt;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.RepExt = value;
}
}
public string AddExt
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.AddExt;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.AddExt = value;
}
}
public bool RemExt
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.RemExt;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.RemExt = value;
}
}
public bool GameName
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.GameName;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.GameName = value;
}
}
public bool Romba
{
get
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
return _datHeader.Romba;
}
set
{
if (_datHeader == null)
{
_datHeader = new DatHeader();
}
_datHeader.Romba = value;
}
}
// Statistical data related to the DAT
public long Count
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.Count;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.Count = value;
}
}
public long ArchiveCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.ArchiveCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.ArchiveCount = value;
}
}
public long BiosSetCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.BiosSetCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.BiosSetCount = value;
}
}
public long DiskCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.DiskCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.DiskCount = value;
}
}
public long ReleaseCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.ReleaseCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.ReleaseCount = value;
}
}
public long RomCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.RomCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.RomCount = value;
}
}
public long SampleCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.SampleCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.SampleCount = value;
}
}
public long TotalSize
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.TotalSize;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.TotalSize = value;
}
}
public long CRCCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.CRCCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.CRCCount = value;
}
}
public long MD5Count
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.MD5Count;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.MD5Count = value;
}
}
public long SHA1Count
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.SHA1Count;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.SHA1Count = value;
}
}
public long SHA256Count
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.SHA256Count;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.SHA256Count = value;
}
}
public long SHA384Count
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.SHA384Count;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.SHA384Count = value;
}
}
public long SHA512Count
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.SHA512Count;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.SHA512Count = value;
}
}
public long BaddumpCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.BaddumpCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.BaddumpCount = value;
}
}
public long GoodCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.GoodCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.GoodCount = value;
}
}
public long NodumpCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.NodumpCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.NodumpCount = value;
}
}
public long VerifiedCount
{
get
{
if (_datStats == null)
{
_datStats = new DatStats();
}
return _datStats.VerifiedCount;
}
private set
{
if (_datStats == null)
{
_datStats = new DatStats();
}
_datStats.VerifiedCount = value;
}
}
#endregion
#region Instance Methods
#region Accessors
/// <summary>
/// Passthrough to access the file dictionary
/// </summary>
/// <param name="key">Key in the dictionary to reference</param>
/// <remarks>We don't want to allow direct setting of values because it bypasses the statistics</remarks>
public List<DatItem> this[string key]
{
get
{
// If the dictionary is null, create it
if (_items == null)
{
_items = new SortedDictionary<string, List<DatItem>>();
}
lock (_items)
{
// If the key is missing from the dictionary, add it
if (!_items.ContainsKey(key))
{
_items.Add(key, new List<DatItem>());
}
// Now return the value
return _items[key];
}
}
}
/// <summary>
/// Add a new key to the file dictionary
/// </summary>
/// <param name="key">Key in the dictionary to add</param>
public void Add(string key)
{
// If the dictionary is null, create it
if (_items == null)
{
_items = new SortedDictionary<string, List<DatItem>>();
}
lock (_items)
{
// If the key is missing from the dictionary, add it
if (!_items.ContainsKey(key))
{
_items.Add(key, new List<DatItem>());
}
}
}
/// <summary>
/// Add a value to the file dictionary
/// </summary>
/// <param name="key">Key in the dictionary to add to</param>
/// <param name="value">Value to add to the dictionary</param>
public void Add(string key, DatItem value)
{
// If the dictionary is null, create it
if (_items == null)
{
_items = new SortedDictionary<string, List<DatItem>>();
}
// Add the key, if necessary
Add(key);
lock (_items)
{
// Now add the value
_items[key].Add(value);
// Now update the statistics
_datStats.AddItem(value);
}
}
/// <summary>
/// Add a range of values to the file dictionary
/// </summary>
/// <param name="key">Key in the dictionary to add to</param>
/// <param name="value">Value to add to the dictionary</param>
public void AddRange(string key, List<DatItem> value)
{
// If the dictionary is null, create it
if (_items == null)
{
_items = new SortedDictionary<string, List<DatItem>>();
}
// Add the key, if necessary
Add(key);
lock (_items)
{
// Now add the value
_items[key].AddRange(value);
// Now update the statistics
foreach (DatItem item in value)
{
_datStats.AddItem(item);
}
}
}
/// <summary>
/// Get if the file dictionary contains the key
/// </summary>
/// <param name="key">Key in the dictionary to check</param>
/// <returns>True if the key exists, false otherwise</returns>
public bool Contains(string key)
{
bool contains = false;
// If the dictionary is null, create it
if (_items == null)
{
_items = new SortedDictionary<string, List<DatItem>>();
}
// If the key is null, we return false since keys can't be null
if (key == null)
{
return contains;
}
lock (_items)
{
contains = _items.ContainsKey(key);
}
return contains;
}
/// <summary>
/// Get if the file dictionary contains the key and value
/// </summary>
/// <param name="key">Key in the dictionary to check</param>
/// <param name="value">Value in the dictionary to check</param>
/// <returns>True if the key exists, false otherwise</returns>
public bool Contains(string key, DatItem value)
{
bool contains = false;
// If the dictionary is null, create it
if (_items == null)
{
_items = new SortedDictionary<string, List<DatItem>>();
}
// If the key is null, we return false since keys can't be null
if (key == null)
{
return contains;
}
lock (_items)
{
if (_items.ContainsKey(key))
{
contains = _items.ContainsKey(key);
}
}
return contains;
}
/// <summary>
/// Get the keys from the file dictionary
/// </summary>
/// <returns>IEnumerable of the keys</returns>
public List<string> Keys
{
get
{
// If the dictionary is null, create it
if (_items == null)
{
_items = new SortedDictionary<string, List<DatItem>>();
}
lock (_items)
{
return _items.Keys.ToList();
}
}
}
/// <summary>
/// Remove a key from the file dictionary if it exists
/// </summary>
/// <param name="key">Key in the dictionary to remove</param>
public void Remove(string key)
{
// If the dictionary is null, create it
if (_items == null)
{
_items = new SortedDictionary<string, List<DatItem>>();
}
// If the key doesn't exist, return
if (!Contains(key))
{
return;
}
lock (_items)
{
// Remove the statistics first
foreach (DatItem item in _items[key])
{
_datStats.RemoveItem(item);
}
// Remove the key from the dictionary
_items.Remove(key);
}
}
/// <summary>
/// Remove a value from the file dictionary if it exists
/// </summary>
/// <param name="key">Key in the dictionary to remove from</param>
/// <param name="value">Value to remove from the dictionary</param>
public void Remove(string key, DatItem value)
{
// If the dictionary is null, create it
if (_items == null)
{
_items = new SortedDictionary<string, List<DatItem>>();
}
// If the key and value doesn't exist, return
if (!Contains(key, value))
{
return;
}
lock (_items)
{
// While the key is in the dictionary and the item is there, remove it
while (_items.ContainsKey(key) && _items[key].Contains(value))
{
// Remove the statistics first
_datStats.RemoveItem(value);
_items[key].Remove(value);
}
}
}
/// <summary>
/// Remove a range of values from the file dictionary if they exists
/// </summary>
/// <param name="key">Key in the dictionary to remove from</param>
/// <param name="value">Value to remove from the dictionary</param>
public void RemoveRange(string key, List<DatItem> value)
{
foreach(DatItem item in value)
{
Remove(key, item);
}
}
#endregion
#region Bucketing
/// <summary>
/// Take the arbitrarily sorted Files Dictionary and convert to one sorted by a user-defined method
/// </summary>
/// <param name="bucketBy">SortedBy enum representing how to sort the individual items</param>
/// <param name="deduperoms">Dedupe type that should be used</param>
/// <param name="lower">True if the key should be lowercased (default), false otherwise</param>
/// <param name="norename">True if games should only be compared on game and file name, false if system and source are counted</param>
public void BucketBy(SortedBy bucketBy, DedupeType deduperoms, 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)
{
return;
}
Globals.Logger.User("Organizing roms by {0}" + (deduperoms != DedupeType.None ? " and merging" : ""), bucketBy);
// If the sorted type isn't the same, we want to sort the dictionary accordingly
if (_sortedBy != bucketBy)
{
// Set the sorted type
_sortedBy = bucketBy;
// First do the initial sort of all of the roms inplace
List<string> oldkeys = Keys;
Parallel.ForEach(oldkeys, Globals.ParallelOptions, key =>
{
// Get the unsorted current list
List<DatItem> roms = this[key];
// Now add each of the roms to their respective games
foreach (DatItem rom in roms)
{
// We want to get the key most appropriate for the given sorting type
string newkey = GetKey(rom, bucketBy, lower, norename);
// Add the DatItem to the dictionary
Add(newkey, rom);
}
// Finally, remove the entire original key
Remove(key);
});
}
// Now go through and sort all of the individual lists
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
// Get the possibly unsorted list
List<DatItem> sortedlist = this[key];
// Sort the list of items to be consistent
DatItem.Sort(ref sortedlist, false);
// If we're merging the roms, do so
if (deduperoms == DedupeType.Full || (deduperoms == DedupeType.Game && bucketBy == SortedBy.Game))
{
sortedlist = DatItem.Merge(sortedlist);
}
// Add the list back to the dictionary
Remove(key);
AddRange(key, sortedlist);
});
}
/// <summary>
/// Get the dictionary key that should be used for a given item and sorting type
/// </summary>
/// <param name="item">DatItem to get the key for</param>
/// <param name="sortedBy">SortedBy enum representing what key to get</param>
/// <param name="lower">True if the key should be lowercased (default), false otherwise</param>
/// <param name="norename">True if games should only be compared on game and file name, false if system and source are counted</param>
/// <returns>String representing the key to be used for the DatItem</returns>
private string GetKey(DatItem item, SortedBy sortedBy, bool lower = true, bool norename = true)
{
// Set the output key as the default blank string
string key = "";
// Now determine what the key should be based on the sortedBy value
switch (sortedBy)
{
case SortedBy.CRC:
key = (item.Type == ItemType.Rom ? ((Rom)item).CRC : Constants.CRCZero);
break;
case SortedBy.Game:
key = (norename ? ""
: item.SystemID.ToString().PadLeft(10, '0')
+ "-"
+ item.SourceID.ToString().PadLeft(10, '0') + "-")
+ (String.IsNullOrEmpty(item.MachineName)
? "Default"
: item.MachineName);
if (lower)
{
key = key.ToLowerInvariant();
}
if (key == null)
{
key = "null";
}
key = HttpUtility.HtmlEncode(key);
break;
case SortedBy.MD5:
key = (item.Type == ItemType.Rom
? ((Rom)item).MD5
: (item.Type == ItemType.Disk
? ((Disk)item).MD5
: Constants.MD5Zero));
break;
case SortedBy.SHA1:
key = (item.Type == ItemType.Rom
? ((Rom)item).SHA1
: (item.Type == ItemType.Disk
? ((Disk)item).SHA1
: Constants.SHA1Zero));
break;
case SortedBy.SHA256:
key = (item.Type == ItemType.Rom
? ((Rom)item).SHA256
: (item.Type == ItemType.Disk
? ((Disk)item).SHA256
: Constants.SHA256Zero));
break;
case SortedBy.SHA384:
key = (item.Type == ItemType.Rom
? ((Rom)item).SHA384
: (item.Type == ItemType.Disk
? ((Disk)item).SHA384
: Constants.SHA384Zero));
break;
case SortedBy.SHA512:
key = (item.Type == ItemType.Rom
? ((Rom)item).SHA512
: (item.Type == ItemType.Disk
? ((Disk)item).SHA512
: Constants.SHA512Zero));
break;
}
// Double and triple check the key for corner cases
if (key == null)
{
key = "";
}
return key;
}
#endregion
#region Constructors
/// <summary>
/// Create a new, empty DatFile object
/// </summary>
public DatFile()
{
_items = new SortedDictionary<string, List<DatItem>>();
}
/// <summary>
/// Create a new DatFile from an existing one using the header values only
/// </summary>
/// <param name="df"></param>
public DatFile(DatFile datFile)
{
_datHeader = (DatHeader)datFile._datHeader.Clone();
}
#endregion
#region Converting and Updating
/// <summary>
/// Determine if input files should be merged, diffed, or processed invidually
/// </summary>
/// <param name="inputPaths">Names of the input files and/or folders</param>
/// <param name="basePaths">Names of base files and/or folders</param>
/// <param name="outDir">Optional param for output directory</param>
/// <param name="merge">True if input files should be merged into a single file, false otherwise</param>
/// <param name="updateMode">Non-zero flag for diffing mode, zero otherwise</param>
/// <param name="inplace">True if the output files should overwrite their inputs, false otherwise</param>
/// <param name="skip">True if the first cascaded diff file should be skipped on output, false otherwise</param>
/// <param name="bare">True if the date should not be appended to the default name, false otherwise [OBSOLETE]</param>
/// <param name="clean">True to clean the game names to WoD standard, false otherwise (default)</param>
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
/// <param name="descAsName">True to allow SL DATs to have game names used instead of descriptions, false otherwise (default)</param>
/// <param name="filter">Filter object to be passed to the DatItem level</param>
/// <param name="splitType">Type of the split that should be performed (split, merged, fully merged)</param>
/// <param name="trim">True if we are supposed to trim names to NTFS length, false otherwise</param>
/// <param name="single">True if all games should be replaced by '!', false otherwise</param>
/// <param name="root">String representing root directory to compare against for length calculation</param>
public void DetermineUpdateType(List<string> inputPaths, List<string> basePaths, string outDir, bool merge, UpdateMode updateMode, bool inplace, bool skip,
bool bare, bool clean, bool remUnicode, bool descAsName, Filter filter, SplitType splitType, bool trim, bool single, string root)
{
// If we're in merging or diffing mode, use the full list of inputs
if (merge || (updateMode != UpdateMode.None
&& (updateMode & UpdateMode.DiffAgainst) == 0)
&& (updateMode & UpdateMode.BaseReplace) == 0
&& (updateMode & UpdateMode.ReverseBaseReplace) == 0)
{
// Make sure there are no folders in inputs
List<string> newInputFileNames = FileTools.GetOnlyFilesFromInputs(inputPaths, appendparent: true);
// Reverse if we have to
if ((updateMode & UpdateMode.DiffReverseCascade) != 0)
{
newInputFileNames.Reverse();
}
// Create a dictionary of all ROMs from the input DATs
List<DatFile> datHeaders = PopulateUserData(newInputFileNames, inplace, clean,
remUnicode, descAsName, outDir, filter, splitType, trim, single, root);
// Modify the Dictionary if necessary and output the results
if (updateMode != 0 && updateMode < UpdateMode.DiffCascade)
{
DiffNoCascade(updateMode, outDir, newInputFileNames);
}
// If we're in cascade and diff, output only cascaded diffs
else if (updateMode != 0 && updateMode >= UpdateMode.DiffCascade)
{
DiffCascade(outDir, inplace, newInputFileNames, datHeaders, skip);
}
// Output all entries with user-defined merge
else
{
MergeNoDiff(outDir, newInputFileNames, datHeaders);
}
}
// If we're in "diff against" mode, we treat the inputs differently
else if ((updateMode & UpdateMode.DiffAgainst) != 0)
{
DiffAgainst(inputPaths, basePaths, outDir, inplace, clean, remUnicode, descAsName, filter, splitType, trim, single, root);
}
// If we're in "base replacement" mode, we treat the inputs differently
else if ((updateMode & UpdateMode.BaseReplace) != 0)
{
BaseReplace(inputPaths, basePaths, outDir, inplace, clean, remUnicode, descAsName, filter, splitType, trim, single, root, false);
}
// If we're in "reverse base replacement" mode, we treat the inputs differently
else if ((updateMode & UpdateMode.ReverseBaseReplace) != 0)
{
BaseReplace(inputPaths, basePaths, outDir, inplace, clean, remUnicode, descAsName, filter, splitType, trim, single, root, true);
}
// Otherwise, loop through all of the inputs individually
else
{
Update(inputPaths, outDir, inplace, clean, remUnicode, descAsName, filter, splitType, trim, single, root);
}
return;
}
/// <summary>
/// Populate the user DatData object from the input files
/// </summary>
/// <param name="inputs">Paths to DATs to parse</param>
/// <param name="inplace">True if the output files should overwrite their inputs, false otherwise</param>
/// <param name="clean">True to clean the game names to WoD standard, false otherwise (default)</param>
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
/// <param name="descAsName">True to allow SL DATs to have game names used instead of descriptions, false otherwise (default)</param>
/// <param name="splitType">Type of the split that should be performed (split, merged, fully merged)</param>
/// <param name="outDir">Optional param for output directory</param>
/// <param name="filter">Filter object to be passed to the DatItem level</param>
/// <param name="splitType">Type of the split that should be performed (split, merged, fully merged)</param>
/// <param name="trim">True if we are supposed to trim names to NTFS length, false otherwise</param>
/// <param name="single">True if all games should be replaced by '!', false otherwise</param>
/// <param name="root">String representing root directory to compare against for length calculation</param>
/// <returns>List of DatData objects representing headers</returns>
private List<DatFile> PopulateUserData(List<string> inputs, bool inplace, bool clean, bool remUnicode, bool descAsName,
string outDir, Filter filter, SplitType splitType, bool trim, bool single, string root)
{
DatFile[] datHeaders = new DatFile[inputs.Count];
InternalStopwatch watch = new InternalStopwatch("Processing individual DATs");
// Parse all of the DATs into their own DatFiles in the array
Parallel.For(0, inputs.Count, Globals.ParallelOptions, i =>
{
string input = inputs[i];
Globals.Logger.User("Adding DAT: {0}", input.Split('¬')[0]);
datHeaders[i] = new DatFile
{
DatFormat = (DatFormat != 0 ? DatFormat : 0),
DedupeRoms = DedupeRoms,
};
datHeaders[i].Parse(input.Split('¬')[0], i, 0, splitType, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName);
});
watch.Stop();
watch.Start("Populating internal DAT");
Parallel.For(0, inputs.Count, Globals.ParallelOptions, i =>
{
// Get the list of keys from the DAT
List<string> 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 souce DAT to save memory
datHeaders[i].DeleteDictionary();
});
// Now that we have a merged DAT, filter it
Filter(filter, single, trim, root);
watch.Stop();
return datHeaders.ToList();
}
/// <summary>
/// Replace item names from on a base set
/// </summary>
/// <param name="inputPaths">Names of the input files and/or folders</param>
/// <param name="basePaths">Names of base files and/or folders</param>
/// <param name="outDir">Optional param for output directory</param>
/// <param name="inplace">True if the output files should overwrite their inputs, false otherwise</param>
/// <param name="clean">True to clean the game names to WoD standard, false otherwise (default)</param>
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
/// <param name="descAsName">True to allow SL DATs to have game names used instead of descriptions, false otherwise (default)</param>
/// <param name="filter">Filter object to be passed to the DatItem level</param>
/// <param name="splitType">Type of the split that should be performed (split, merged, fully merged)</param>
/// <param name="trim">True if we are supposed to trim names to NTFS length, false otherwise</param>
/// <param name="single">True if all games should be replaced by '!', false otherwise</param>
/// <param name="root">String representing root directory to compare against for length calculation</param>
/// <param name="reverse">True if the base DATs should be reverse-ordered, false otherwise</param>
public void BaseReplace(List<string> inputPaths, List<string> basePaths, string outDir, bool inplace, bool clean, bool remUnicode,
bool descAsName, Filter filter, SplitType splitType, bool trim, bool single, string root, bool reverse)
{
// First we want to parse all of the base DATs into the input
InternalStopwatch watch = new InternalStopwatch("Populating base DAT for replacement...");
List<string> baseFileNames = FileTools.GetOnlyFilesFromInputs(basePaths);
Parallel.For(0, baseFileNames.Count, Globals.ParallelOptions, i =>
{
string path = "";
int id = 0;
lock (baseFileNames)
{
path = baseFileNames[i];
id = (reverse ? i : baseFileNames.Count - i);
}
Parse(path, id, id, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName);
});
watch.Stop();
// For comparison's sake, we want to use CRC as the base ordering
BucketBy(SortedBy.CRC, DedupeType.Full);
// Now we want to try to replace each item in each input DAT from the base
List<string> inputFileNames = FileTools.GetOnlyFilesFromInputs(inputPaths, appendparent: true);
foreach (string path in inputFileNames)
{
// Get the two halves of the path
string[] splitpath = path.Split('¬');
Globals.Logger.User("Replacing items in '{0}'' from the base DAT", splitpath[0]);
// First we parse in the DAT internally
DatFile intDat = new DatFile();
intDat.Parse(splitpath[0], 1, 1, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName);
// For comparison's sake, we want to use CRC as the base ordering
intDat.BucketBy(SortedBy.CRC, DedupeType.Full);
// Then we do a hashwise comparison against the base DAT
List<string> keys = intDat.Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> datItems = intDat[key];
List<DatItem> newDatItems = new List<DatItem>();
foreach (DatItem datItem in datItems)
{
List<DatItem> dupes = datItem.GetDuplicates(this, sorted: true);
DatItem newDatItem = (DatItem)datItem.Clone();
if (dupes.Count > 0)
{
newDatItem.Name = dupes[0].Name;
}
newDatItems.Add(newDatItem);
}
// Now add the new list to the key
intDat.Remove(key);
intDat.AddRange(key, newDatItems);
});
// Determine the output path for the DAT
string interOutDir = outDir;
if (inplace)
{
interOutDir = Path.GetDirectoryName(path);
}
else if (!String.IsNullOrEmpty(interOutDir))
{
if (splitpath[0].Length == splitpath[1].Length)
{
interOutDir = Path.GetDirectoryName(Path.Combine(interOutDir, Path.GetFileName(splitpath[0])));
}
else
{
interOutDir = Path.GetDirectoryName(Path.Combine(interOutDir, splitpath[0].Remove(0, splitpath[1].Length + 1)));
}
}
else
{
if (splitpath[0].Length == splitpath[1].Length)
{
interOutDir = Path.GetDirectoryName(Path.Combine(Environment.CurrentDirectory, Path.GetFileName(splitpath[0])));
}
else
{
interOutDir = Path.GetDirectoryName(Path.Combine(Environment.CurrentDirectory, splitpath[0].Remove(0, splitpath[1].Length + 1)));
}
}
// Once we're done, try writing out
intDat.WriteToFile(interOutDir);
// Due to possible memory requirements, we force a garbage collection
GC.Collect();
}
}
/// <summary>
/// Output diffs against a base set
/// </summary>
/// <param name="inputPaths">Names of the input files and/or folders</param>
/// <param name="basePaths">Names of base files and/or folders</param>
/// <param name="outDir">Optional param for output directory</param>
/// <param name="inplace">True if the output files should overwrite their inputs, false otherwise</param>
/// <param name="clean">True to clean the game names to WoD standard, false otherwise (default)</param>
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
/// <param name="descAsName">True to allow SL DATs to have game names used instead of descriptions, false otherwise (default)</param>
/// <param name="filter">Filter object to be passed to the DatItem level</param>
/// <param name="splitType">Type of the split that should be performed (split, merged, fully merged)</param>
/// <param name="trim">True if we are supposed to trim names to NTFS length, false otherwise</param>
/// <param name="single">True if all games should be replaced by '!', false otherwise</param>
/// <param name="root">String representing root directory to compare against for length calculation</param>
public void DiffAgainst(List<string> inputPaths, List<string> basePaths, string outDir, bool inplace, bool clean, bool remUnicode,
bool descAsName, Filter filter, SplitType splitType, bool trim, bool single, string root)
{
// First we want to parse all of the base DATs into the input
InternalStopwatch watch = new InternalStopwatch("Populating base DAT for comparison...");
List<string> baseFileNames = FileTools.GetOnlyFilesFromInputs(basePaths);
Parallel.ForEach(baseFileNames, Globals.ParallelOptions, path =>
{
Parse(path, 0, 0, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName);
});
watch.Stop();
// For comparison's sake, we want to use CRC as the base ordering
BucketBy(SortedBy.CRC, DedupeType.Full);
// Now we want to compare each input DAT against the base
List<string> inputFileNames = FileTools.GetOnlyFilesFromInputs(inputPaths, appendparent: true);
foreach (string path in inputFileNames)
{
// Get the two halves of the path
string[] splitpath = path.Split('¬');
Globals.Logger.User("Comparing '{0}'' to base DAT", splitpath[0]);
// First we parse in the DAT internally
DatFile intDat = new DatFile();
intDat.Parse(splitpath[0], 1, 1, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName);
// For comparison's sake, we want to use CRC as the base ordering
intDat.BucketBy(SortedBy.CRC, DedupeType.Full);
// Then we do a hashwise comparison against the base DAT
List<string> keys = intDat.Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> datItems = intDat[key];
List<DatItem> keepDatItems = new List<DatItem>();
foreach (DatItem datItem in datItems)
{
if (!datItem.HasDuplicates(this, true))
{
keepDatItems.Add(datItem);
}
}
// Now add the new list to the key
intDat.Remove(key);
intDat.AddRange(key, keepDatItems);
});
// Determine the output path for the DAT
string interOutDir = outDir;
if (inplace)
{
interOutDir = Path.GetDirectoryName(path);
}
else if (!String.IsNullOrEmpty(interOutDir))
{
if (splitpath[0].Length == splitpath[1].Length)
{
interOutDir = Path.GetDirectoryName(Path.Combine(interOutDir, Path.GetFileName(splitpath[0])));
}
else
{
interOutDir = Path.GetDirectoryName(Path.Combine(interOutDir, splitpath[0].Remove(0, splitpath[1].Length + 1)));
}
}
else
{
if (splitpath[0].Length == splitpath[1].Length)
{
interOutDir = Path.GetDirectoryName(Path.Combine(Environment.CurrentDirectory, Path.GetFileName(splitpath[0])));
}
else
{
interOutDir = Path.GetDirectoryName(Path.Combine(Environment.CurrentDirectory, splitpath[0].Remove(0, splitpath[1].Length + 1)));
}
}
// Once we're done, try writing out
intDat.WriteToFile(interOutDir);
// Due to possible memory requirements, we force a garbage collection
GC.Collect();
}
}
/// <summary>
/// Output cascading diffs
/// </summary>
/// <param name="outDir">Output directory to write the DATs to</param>
/// <param name="inplace">True if cascaded diffs are outputted in-place, false otherwise</param>
/// <param name="inputs">List of inputs to write out from</param>
/// <param name="datHeaders">Dat headers used optionally</param>
/// <param name="skip">True if the first cascaded diff file should be skipped on output, false otherwise</param>
public void DiffCascade(string outDir, bool inplace, List<string> inputs, List<DatFile> datHeaders, bool skip)
{
string post = "";
// Create a list of DatData objects representing output files
List<DatFile> outDats = new List<DatFile>();
// Loop through each of the inputs and get or create a new DatData object
InternalStopwatch watch = new InternalStopwatch("Initializing all output DATs");
DatFile[] outDatsArray = new DatFile[inputs.Count];
Parallel.For(0, inputs.Count, Globals.ParallelOptions, j =>
{
string innerpost = " (" + Path.GetFileNameWithoutExtension(inputs[j].Split('¬')[0]) + " Only)";
DatFile diffData;
// If we're in inplace mode, take the appropriate DatData object already stored
if (inplace || outDir != Environment.CurrentDirectory)
{
diffData = datHeaders[j];
}
else
{
diffData = new DatFile(this);
diffData.FileName += post;
diffData.Name += post;
diffData.Description += post;
}
diffData.ResetDictionary();
outDatsArray[j] = diffData;
});
outDats = outDatsArray.ToList();
watch.Stop();
// Now, loop through the dictionary and populate the correct DATs
watch.Start("Populating all output DATs");
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> items = DatItem.Merge(this[key]);
// If the rom list is empty or null, just skip it
if (items == null || items.Count == 0)
{
return;
}
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)
{
Globals.Logger.Warning("Item found with a <0 SystemID: {0}", item.Name);
continue;
}
outDats[item.SystemID].Add(key, item);
}
});
watch.Stop();
// Finally, loop through and output each of the DATs
watch.Start("Outputting all created DATs");
Parallel.For((skip ? 1 : 0), inputs.Count, Globals.ParallelOptions, j =>
{
// If we have an output directory set, replace the path
string path = "";
if (inplace)
{
path = Path.GetDirectoryName(inputs[j].Split('¬')[0]);
}
else if (outDir != Environment.CurrentDirectory)
{
string[] split = inputs[j].Split('¬');
path = outDir + (split[0] == split[1]
? Path.GetFileName(split[0])
: (Path.GetDirectoryName(split[0]).Remove(0, split[1].Length))); ;
}
// Try to output the file
outDats[j].WriteToFile(path);
});
watch.Stop();
}
/// <summary>
/// Output non-cascading diffs
/// </summary>
/// <param name="diff">Non-zero flag for diffing mode, zero otherwise</param>
/// <param name="outDir">Output directory to write the DATs to</param>
/// <param name="inputs">List of inputs to write out from</param>
public void DiffNoCascade(UpdateMode diff, string outDir, List<string> inputs)
{
InternalStopwatch watch = new InternalStopwatch("Initializing all output DATs");
// Default vars for use
string post = "";
DatFile outerDiffData = new DatFile();
DatFile dupeData = new DatFile();
// Fill in any information not in the base DAT
if (String.IsNullOrEmpty(FileName))
{
FileName = "All DATs";
}
if (String.IsNullOrEmpty(Name))
{
Name = "All DATs";
}
if (String.IsNullOrEmpty(Description))
{
Description = "All DATs";
}
// Don't have External dupes
if ((diff & UpdateMode.DiffNoDupesOnly) != 0)
{
post = " (No Duplicates)";
outerDiffData = new DatFile(this);
outerDiffData.FileName += post;
outerDiffData.Name += post;
outerDiffData.Description += post;
outerDiffData.ResetDictionary();
}
// Have External dupes
if ((diff & UpdateMode.DiffDupesOnly) != 0)
{
post = " (Duplicates)";
dupeData = new DatFile(this);
dupeData.FileName += post;
dupeData.Name += post;
dupeData.Description += post;
dupeData.ResetDictionary();
}
// Create a list of DatData objects representing individual output files
List<DatFile> outDats = new List<DatFile>();
// Loop through each of the inputs and get or create a new DatData object
if ((diff & UpdateMode.DiffIndividualsOnly) != 0)
{
DatFile[] outDatsArray = new DatFile[inputs.Count];
Parallel.For(0, inputs.Count, Globals.ParallelOptions, j =>
{
string innerpost = " (" + Path.GetFileNameWithoutExtension(inputs[j].Split('¬')[0]) + " Only)";
DatFile diffData = new DatFile(this);
diffData.FileName += innerpost;
diffData.Name += innerpost;
diffData.Description += innerpost;
diffData.ResetDictionary();
outDatsArray[j] = diffData;
});
outDats = outDatsArray.ToList();
}
watch.Stop();
// Now, loop through the dictionary and populate the correct DATs
watch.Start("Populating all output DATs");
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> items = DatItem.Merge(this[key]);
// If the rom list is empty or null, just skip it
if (items == null || items.Count == 0)
{
return;
}
// Loop through and add the items correctly
foreach (DatItem item in items)
{
// No duplicates
if ((diff & UpdateMode.DiffNoDupesOnly) != 0 || (diff & UpdateMode.DiffIndividualsOnly) != 0)
{
if ((item.Dupe & DupeType.Internal) != 0)
{
// Individual DATs that are output
if ((diff & UpdateMode.DiffIndividualsOnly) != 0)
{
outDats[item.SystemID].Add(key, item);
}
// Merged no-duplicates DAT
if ((diff & UpdateMode.DiffNoDupesOnly) != 0)
{
DatItem newrom = item.Clone() as DatItem;
newrom.MachineName += " (" + Path.GetFileNameWithoutExtension(inputs[newrom.SystemID].Split('¬')[0]) + ")";
outerDiffData.Add(key, newrom);
}
}
}
// Duplicates only
if ((diff & UpdateMode.DiffDupesOnly) != 0)
{
if ((item.Dupe & DupeType.External) != 0)
{
DatItem newrom = item.Clone() as DatItem;
newrom.MachineName += " (" + Path.GetFileNameWithoutExtension(inputs[newrom.SystemID].Split('¬')[0]) + ")";
dupeData.Add(key, newrom);
}
}
}
});
watch.Stop();
// Finally, loop through and output each of the DATs
watch.Start("Outputting all created DATs");
// Output the difflist (a-b)+(b-a) diff
if ((diff & UpdateMode.DiffNoDupesOnly) != 0)
{
outerDiffData.WriteToFile(outDir);
}
// Output the (ab) diff
if ((diff & UpdateMode.DiffDupesOnly) != 0)
{
dupeData.WriteToFile(outDir);
}
// Output the individual (a-b) DATs
if ((diff & UpdateMode.DiffIndividualsOnly) != 0)
{
Parallel.For(0, inputs.Count, Globals.ParallelOptions, j =>
{
// If we have an output directory set, replace the path
string[] split = inputs[j].Split('¬');
string path = Path.Combine(outDir,
(split[0] == split[1]
? Path.GetFileName(split[0])
: (Path.GetDirectoryName(split[0]).Remove(0, split[1].Length))));
// Try to output the file
outDats[j].WriteToFile(path);
});
}
watch.Stop();
}
/// <summary>
/// Output user defined merge
/// </summary>
/// <param name="outDir">Output directory to write the DATs to</param>
/// <param name="inputs">List of inputs to write out from</param>
/// <param name="datHeaders">Dat headers used optionally</param>
public void MergeNoDiff(string outDir, List<string> inputs, List<DatFile> datHeaders)
{
// If we're in SuperDAT mode, prefix all games with their respective DATs
if (Type == "SuperDAT")
{
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> items = this[key].ToList();
List<DatItem> newItems = new List<DatItem>();
foreach (DatItem item in items)
{
DatItem newItem = item;
string filename = inputs[newItem.SystemID].Split('¬')[0];
string rootpath = inputs[newItem.SystemID].Split('¬')[1];
rootpath += (rootpath == "" ? "" : Path.DirectorySeparatorChar.ToString());
filename = filename.Remove(0, rootpath.Length);
newItem.MachineName = Path.GetDirectoryName(filename) + Path.DirectorySeparatorChar
+ Path.GetFileNameWithoutExtension(filename) + Path.DirectorySeparatorChar
+ newItem.MachineName;
newItems.Add(newItem);
}
Remove(key);
AddRange(key, newItems);
});
}
// Try to output the file
WriteToFile(outDir);
}
/// <summary>
/// Convert, update, and filter a DAT file or set of files using a base
/// </summary>
/// <param name="inputFileNames">Names of the input files and/or folders</param>
/// <param name="outDir">Optional param for output directory</param>
/// <param name="merge">True if input files should be merged into a single file, false otherwise</param>
/// <param name="diff">Non-zero flag for diffing mode, zero otherwise</param>
/// <param name="inplace">True if the output files should overwrite their inputs, false otherwise</param>
/// <param name="skip">True if the first cascaded diff file should be skipped on output, false otherwise</param>
/// <param name="bare">True if the date should not be appended to the default name, false otherwise [OBSOLETE]</param>
/// <param name="clean">True to clean the game names to WoD standard, false otherwise (default)</param>
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
/// <param name="descAsName">True to allow SL DATs to have game names used instead of descriptions, false otherwise (default)</param>
/// <param name="filter">Filter object to be passed to the DatItem level</param>
/// <param name="splitType">Type of the split that should be performed (split, merged, fully merged)</param>
/// <param name="trim">True if we are supposed to trim names to NTFS length, false otherwise</param>
/// <param name="single">True if all games should be replaced by '!', false otherwise</param>
/// <param name="root">String representing root directory to compare against for length calculation</param>
public void Update(List<string> inputFileNames, string outDir, bool inplace, bool clean, bool remUnicode, bool descAsName,
Filter filter, SplitType splitType, bool trim, bool single, string root)
{
for (int i = 0; i < inputFileNames.Count; i++)
{
// Get the input file name
string inputFileName = inputFileNames[i];
// Clean the input string
if (inputFileName != "")
{
inputFileName = Path.GetFullPath(inputFileName);
}
if (File.Exists(inputFileName))
{
// If inplace is set, override the output dir
string realOutDir = outDir;
if (inplace)
{
realOutDir = Path.GetDirectoryName(inputFileName);
}
DatFile innerDatdata = new DatFile(this);
Globals.Logger.User("Processing '{0}'", Path.GetFileName(inputFileName));
innerDatdata.Parse(inputFileName, 0, 0, splitType, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName,
keepext: ((innerDatdata.DatFormat & DatFormat.TSV) != 0 || (innerDatdata.DatFormat & DatFormat.CSV) != 0));
innerDatdata.Filter(filter, trim, single, root);
// Try to output the file
innerDatdata.WriteToFile((realOutDir == Environment.CurrentDirectory ? Path.GetDirectoryName(inputFileName) : realOutDir), overwrite: (realOutDir != Environment.CurrentDirectory));
}
else if (Directory.Exists(inputFileName))
{
inputFileName = Path.GetFullPath(inputFileName) + Path.DirectorySeparatorChar;
// If inplace is set, override the output dir
string realOutDir = outDir;
if (inplace)
{
realOutDir = Path.GetDirectoryName(inputFileName);
}
List<string> subFiles = Directory.EnumerateFiles(inputFileName, "*", SearchOption.AllDirectories).ToList();
Parallel.ForEach(subFiles, Globals.ParallelOptions, file =>
{
Globals.Logger.User("Processing '{0}'", Path.GetFullPath(file).Remove(0, inputFileName.Length));
DatFile innerDatdata = new DatFile(this);
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.Filter(filter, trim, single, root);
// Try to output the file
innerDatdata.WriteToFile((realOutDir == Environment.CurrentDirectory ? Path.GetDirectoryName(file) : realOutDir + Path.GetDirectoryName(file).Remove(0, inputFileName.Length - 1)),
overwrite: (realOutDir != Environment.CurrentDirectory));
});
}
else
{
Globals.Logger.Error("I'm sorry but '{0}' doesn't exist!", inputFileName);
return;
}
}
}
#endregion
#region Dictionary Manipulation
/// <summary>
/// Clones the files dictionary
/// </summary>
/// <returns>A new files dictionary instance</returns>
public SortedDictionary<string, List<DatItem>> CloneDictionary()
{
// Create the placeholder dictionary to be used
SortedDictionary<string, List<DatItem>> sorted = new SortedDictionary<string, List<DatItem>>();
// Now perform a deep clone on the entire dictionary
List<string> keys = Keys;
foreach (string key in keys)
{
// Clone each list of DATs in the dictionary
List<DatItem> olditems = this[key];
List<DatItem> newitems = new List<DatItem>();
foreach (DatItem item in olditems)
{
newitems.Add((DatItem)item.Clone());
}
// If the key is missing from the new dictionary, add it
if (!sorted.ContainsKey(key))
{
sorted.Add(key, new List<DatItem>());
}
// Now add the list of items
sorted[key].AddRange(newitems);
}
return sorted;
}
/// <summary>
/// Delete the file dictionary
/// </summary>
public void DeleteDictionary()
{
_items = null;
// Reset statistics
_datStats.Reset();
}
/// <summary>
/// Reset the file dictionary
/// </summary>
public void ResetDictionary()
{
_items = new SortedDictionary<string, List<DatItem>>();
// Reset statistics
_datStats.Reset();
}
#endregion
#region Filtering
/// <summary>
/// Filter a DAT based on input parameters and modify the items
/// </summary>
/// <param name="filter">Filter object for passing to the DatItem level</param>
/// <param name="trim">True if we are supposed to trim names to NTFS length, false otherwise</param>
/// <param name="single">True if all games should be replaced by '!', false otherwise</param>
/// <param name="root">String representing root directory to compare against for length calculation</param>
public void Filter(Filter filter, bool single, bool trim, string root)
{
try
{
// Loop over every key in the dictionary
List<string> keys = Keys;
foreach (string key in keys)
{
// For every item in the current key
List<DatItem> items = this[key];
List<DatItem> newitems = new List<DatItem>();
foreach (DatItem item in items)
{
// If the rom passes the filter, include it
if (filter.ItemPasses(item))
{
// If we are in single game mode, rename all games
if (single)
{
item.MachineName = "!";
}
// If we are in NTFS trim mode, trim the game name
if (trim)
{
// Windows max name length is 260
int usableLength = 260 - item.MachineName.Length - root.Length;
if (item.Name.Length > usableLength)
{
string ext = Path.GetExtension(item.Name);
item.Name = item.Name.Substring(0, usableLength - ext.Length);
item.Name += ext;
}
}
// Lock the list and add the item back
lock (newitems)
{
newitems.Add(item);
}
}
}
Remove(key);
AddRange(key, newitems);
}
}
catch (Exception ex)
{
Globals.Logger.Error(ex.ToString());
}
}
/// <summary>
/// Use game descriptions as names in the DAT, updating cloneof/romof/sampleof
/// </summary>
private void MachineDescriptionToName()
{
try
{
// First we want to get a mapping for all games to description
ConcurrentDictionary<string, string> mapping = new ConcurrentDictionary<string, string>();
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> 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("\"", "''"));
}
}
});
// Now we loop through every item and update accordingly
keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> items = this[key];
List<DatItem> newItems = new List<DatItem>();
foreach (DatItem item in items)
{
// Update machine name
if (!String.IsNullOrEmpty(item.MachineName) && mapping.ContainsKey(item.MachineName))
{
item.MachineName = mapping[item.MachineName];
}
// Update cloneof
if (!String.IsNullOrEmpty(item.CloneOf) && mapping.ContainsKey(item.CloneOf))
{
item.CloneOf = mapping[item.CloneOf];
}
// Update romof
if (!String.IsNullOrEmpty(item.RomOf) && mapping.ContainsKey(item.RomOf))
{
item.RomOf = mapping[item.RomOf];
}
// Update sampleof
if (!String.IsNullOrEmpty(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());
}
}
/// <summary>
/// Remove all items marked for removal from the DAT
/// </summary>
private void RemoveMarkedItems()
{
List<string> keys = Keys;
foreach (string key in keys)
{
List<DatItem> items = this[key];
List<DatItem> newItems = new List<DatItem>();
foreach (DatItem item in items)
{
if (!item.Remove)
{
newItems.Add(item);
}
}
Remove(key);
AddRange(key, newItems);
}
}
/// <summary>
/// Strip the given hash types from the DAT
/// </summary>
private void StripHashesFromItems()
{
// Output the logging statement
Globals.Logger.User("Stripping requested hashes");
// Now process all of the roms
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> items = this[key];
for (int j = 0; j < items.Count; j++)
{
DatItem item = items[j];
if (item.Type == ItemType.Rom)
{
Rom rom = (Rom)item;
if ((StripHash & Hash.MD5) != 0)
{
rom.MD5 = null;
}
if ((StripHash & Hash.SHA1) != 0)
{
rom.SHA1 = null;
}
if ((StripHash & Hash.SHA256) != 0)
{
rom.SHA256 = null;
}
if ((StripHash & Hash.SHA384) != 0)
{
rom.SHA384 = null;
}
if ((StripHash & Hash.SHA512) != 0)
{
rom.SHA512 = null;
}
items[j] = rom;
}
else if (item.Type == ItemType.Disk)
{
Disk disk = (Disk)item;
if ((StripHash & Hash.MD5) != 0)
{
disk.MD5 = null;
}
if ((StripHash & Hash.SHA1) != 0)
{
disk.SHA1 = null;
}
if ((StripHash & Hash.SHA256) != 0)
{
disk.SHA256 = null;
}
if ((StripHash & Hash.SHA384) != 0)
{
disk.SHA384 = null;
}
if ((StripHash & Hash.SHA512) != 0)
{
disk.SHA512 = null;
}
items[j] = disk;
}
}
Remove(key);
AddRange(key, items);
});
}
/// <summary>
/// Strip the dates from the beginning of scene-style set names
/// </summary>
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<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> 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 Merging/Splitting
/// <summary>
/// Use cdevice_ref tags to get full non-merged sets and remove parenting tags
/// </summary>
/// <param name="mergeroms">Dedupe type to be used</param>
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);
_sortedBy = SortedBy.Default;
// Now we want to loop through all of the games and set the correct information
AddRomsFromDevices();
// Then, remove the romof and cloneof tags so it's not picked up by the manager
RemoveTagsFromChild();
// Finally, remove all sets that are labeled as bios or device
//RemoveBiosAndDeviceSets(logger);
}
/// <summary>
/// Use cloneof tags to create non-merged sets and remove the tags plus using the device_ref tags to get full sets
/// </summary>
/// <param name="mergeroms">Dedupe type to be used</param>
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);
_sortedBy = SortedBy.Default;
// Now we want to loop through all of the games and set the correct information
AddRomsFromDevices();
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();
// Finally, remove all sets that are labeled as bios or device
//RemoveBiosAndDeviceSets(logger);
}
/// <summary>
/// Use cloneof tags to create merged sets and remove the tags
/// </summary>
/// <param name="mergeroms">Dedupe type to be used</param>
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);
_sortedBy = SortedBy.Default;
// 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();
// Finally, remove the romof and cloneof tags so it's not picked up by the manager
RemoveTagsFromChild();
}
/// <summary>
/// Use cloneof tags to create non-merged sets and remove the tags
/// </summary>
/// <param name="mergeroms">Dedupe type to be used</param>
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);
_sortedBy = SortedBy.Default;
// 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();
// Finally, remove the romof and cloneof tags so it's not picked up by the manager
RemoveTagsFromChild();
}
/// <summary>
/// Use cloneof and romof tags to create split sets and remove the tags
/// </summary>
/// <param name="mergeroms">Dedupe type to be used</param>
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);
_sortedBy = SortedBy.Default;
// 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();
// Finally, remove the romof and cloneof tags so it's not picked up by the manager
RemoveTagsFromChild();
}
/// <summary>
/// Use romof tags to add roms to the children
/// </summary>
private void AddRomsFromBios()
{
List<string> 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.IsNullOrEmpty(this[game][0].RomOf))
{
parent = this[game][0].RomOf;
}
// If the parent doesnt exist, we want to continue
if (String.IsNullOrEmpty(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<DatItem> 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);
}
}
}
}
/// <summary>
/// Use device_ref tags to add roms to the children
/// </summary>
private void AddRomsFromDevices()
{
List<string> games = Keys;
foreach (string game in games)
{
// If the game has no devices, we continue
if (this[game][0].Devices == null || this[game][0].Devices.Count == 0)
{
continue;
}
// Determine if the game has any devices or not
List<string> devices = this[game][0].Devices;
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<DatItem> devItems = this[device];
foreach (DatItem item in devItems)
{
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);
}
}
}
}
}
/// <summary>
/// Use cloneof tags to add roms to the children, setting the new romof tag in the process
/// </summary>
private void AddRomsFromParent()
{
List<string> 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.IsNullOrEmpty(this[game][0].CloneOf))
{
parent = this[game][0].CloneOf;
}
// If the parent doesnt exist, we want to continue
if (String.IsNullOrEmpty(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<DatItem> 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);
}
}
// Now we want to get the parent romof tag and put it in each of the items
List<DatItem> items = this[game];
string romof = this[parent][0].RomOf;
foreach (DatItem item in items)
{
item.RomOf = romof;
}
}
}
/// <summary>
/// Use cloneof tags to add roms to the parents, removing the child sets in the process
/// </summary>
private void AddRomsFromChildren()
{
List<string> games = Keys;
foreach (string game in games)
{
// Determine if the game has a parent or not
string parent = null;
if (!String.IsNullOrEmpty(this[game][0].CloneOf))
{
parent = this[game][0].CloneOf;
}
// If there is no parent, then we continue
if (String.IsNullOrEmpty(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<DatItem> 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.Type == ItemType.Disk && (item.MergeTag == null || !this[parent].Select(i => i.Name).Contains(item.MergeTag)))
{
item.CopyMachineInformation(copyFrom);
Add(parent, item);
}
// Otherwise, if the parent doesn't already contain the non-disk, add it
else if (item.Type != ItemType.Disk && !this[parent].Contains(item))
{
// Rename the child so it's in a subfolder
item.Name = item.Name + "\\" + 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);
}
}
/// <summary>
/// Remove all BIOS and device sets
/// </summary>
private void RemoveBiosAndDeviceSets()
{
List<string> games = Keys;
foreach (string game in games)
{
if (this[game].Count > 0
&& (this[game][0].MachineType == MachineType.Bios
|| this[game][0].MachineType == MachineType.Device))
{
Remove(game);
}
}
}
/// <summary>
/// Use romof tags to remove roms from the children
/// </summary>
private void RemoveBiosRomsFromChild()
{
// Loop through the romof tags
List<string> 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.IsNullOrEmpty(this[game][0].RomOf))
{
parent = this[game][0].RomOf;
}
// If the parent doesnt exist, we want to continue
if (String.IsNullOrEmpty(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<DatItem> parentItems = this[parent];
foreach (DatItem item in parentItems)
{
DatItem datItem = (DatItem)item.Clone();
Remove(game, datItem);
}
}
}
/// <summary>
/// Use cloneof tags to remove roms from the children
/// </summary>
private void RemoveRomsFromChild()
{
List<string> 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.IsNullOrEmpty(this[game][0].CloneOf))
{
parent = this[game][0].CloneOf;
}
// If the parent doesnt exist, we want to continue
if (String.IsNullOrEmpty(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
List<DatItem> parentItems = this[parent];
foreach (DatItem item in parentItems)
{
DatItem datItem = (DatItem)item.Clone();
Remove(game, datItem);
}
// Now we want to get the parent romof tag and put it in each of the items
List<DatItem> items = this[game];
string romof = this[parent][0].RomOf;
foreach (DatItem item in items)
{
item.RomOf = romof;
}
}
}
/// <summary>
/// Remove all romof and cloneof tags from all games
/// </summary>
private void RemoveTagsFromChild()
{
List<string> games = Keys;
foreach (string game in games)
{
List<DatItem> items = this[game];
foreach (DatItem item in items)
{
item.CloneOf = null;
item.RomOf = null;
}
}
}
#endregion
#region Parsing
/// <summary>
/// Parse a DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="sysid">System ID for the DAT</param>
/// <param name="srcid">Source ID for the DAT</param>
/// <param name="datdata">The DatData object representing found roms to this point</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="clean">True if game names are sanitized, false otherwise (default)</param>
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
/// <param name="descAsName">True if descriptions should be used as names, false otherwise (default)</param>
/// <param name="keepext">True if original extension should be kept, false otherwise (default)</param>
/// <param name="useTags">True if tags from the DAT should be used to merge the output, false otherwise (default)</param>
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)
{
Parse(filename, sysid, srcid, SplitType.None, keep: keep, clean: clean,
remUnicode: remUnicode, descAsName: descAsName, keepext: keepext, useTags: useTags);
}
/// <summary>
/// Parse a DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="sysid">System ID for the DAT</param>
/// <param name="srcid">Source ID for the DAT</param>>
/// <param name="splitType">Type of the split that should be performed (split, merged, fully merged)</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="clean">True if game names are sanitized, false otherwise (default)</param>
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
/// <param name="descAsName">True if descriptions should be used as names, false otherwise (default)</param>
/// <param name="keepext">True if original extension should be kept, false otherwise (default)</param>
/// <param name="useTags">True if tags from the DAT should be used to merge the output, false otherwise (default)</param>
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)
{
// Check the file extension first as a safeguard
string ext = Path.GetExtension(filename).ToLowerInvariant();
if (ext.StartsWith("."))
{
ext = ext.Substring(1);
}
if (ext != "dat" && ext != "csv" && ext != "md5" && ext != "sfv" && ext != "sha1" && ext != "sha256"
&& ext != "sha384" && ext != "sha512" && ext != "tsv" && ext != "txt" && ext != "xml")
{
return;
}
// If the output filename isn't set already, get the internal filename
FileName = (String.IsNullOrEmpty(FileName) ? (keepext ? Path.GetFileName(filename) : Path.GetFileNameWithoutExtension(filename)) : FileName);
// If the output type isn't set already, get the internal output type
DatFormat = (DatFormat == 0 ? FileTools.GetDatFormat(filename) : DatFormat);
// Now parse the correct type of DAT
try
{
switch (FileTools.GetDatFormat(filename))
{
case DatFormat.AttractMode:
new AttractMode(this).Parse(filename, sysid, srcid, keep, clean, remUnicode);
break;
case DatFormat.ClrMamePro:
new ClrMamePro(this).Parse(filename, sysid, srcid, keep, clean, remUnicode);
break;
case DatFormat.CSV:
new SeparatedValue(this).Parse(filename, sysid, srcid, ',', keep, clean, remUnicode);
break;
case DatFormat.DOSCenter:
new DosCenter(this).Parse(filename, sysid, srcid, keep, clean, remUnicode);
break;
case DatFormat.Listroms:
new Listroms(this).Parse(filename, sysid, srcid, keep, clean, remUnicode);
break;
case DatFormat.Logiqx:
new Logiqx(this).Parse(filename, sysid, srcid, keep, clean, remUnicode);
break;
case DatFormat.OfflineList:
new OfflineList(this).Parse(filename, sysid, srcid, keep, clean, remUnicode);
break;
case DatFormat.RedumpMD5:
new Hashfile(this).Parse(filename, sysid, srcid, Hash.MD5, clean, remUnicode);
break;
case DatFormat.RedumpSFV:
new Hashfile(this).Parse(filename, sysid, srcid, Hash.CRC, clean, remUnicode);
break;
case DatFormat.RedumpSHA1:
new Hashfile(this).Parse(filename, sysid, srcid, Hash.SHA1, clean, remUnicode);
break;
case DatFormat.RedumpSHA256:
new Hashfile(this).Parse(filename, sysid, srcid, Hash.SHA256, clean, remUnicode);
break;
case DatFormat.RedumpSHA384:
new Hashfile(this).Parse(filename, sysid, srcid, Hash.SHA384, clean, remUnicode);
break;
case DatFormat.RedumpSHA512:
new Hashfile(this).Parse(filename, sysid, srcid, Hash.SHA512, clean, remUnicode);
break;
case DatFormat.RomCenter:
new RomCenter(this).Parse(filename, sysid, srcid, clean, remUnicode);
break;
case DatFormat.SabreDat:
new SabreDat(this).Parse(filename, sysid, srcid, keep, clean, remUnicode);
break;
case DatFormat.SoftwareList:
new SoftwareList(this).Parse(filename, sysid, srcid, keep, clean, remUnicode);
break;
case DatFormat.TSV:
new SeparatedValue(this).Parse(filename, sysid, srcid, '\t', keep, clean, remUnicode);
break;
default:
return;
}
}
catch (Exception ex)
{
Globals.Logger.Error("Error with file '{0}': {1}", 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)
{
switch (ForceMerging)
{
case ForceMerging.None:
// No-op
break;
case ForceMerging.Split:
splitType = SplitType.Split;
break;
case ForceMerging.Merged:
splitType = SplitType.Merged;
break;
case ForceMerging.NonMerged:
splitType = SplitType.NonMerged;
break;
case ForceMerging.Full:
splitType = SplitType.FullNonMerged;
break;
}
}
// 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;
}
}
/// <summary>
/// Add a rom to the Dat after checking
/// </summary>
/// <param name="item">Item data to check against</param>
/// <param name="clean">True if the names should be cleaned to WoD standards, false otherwise</param>
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
/// <returns>The key for the item</returns>
public string ParseAddHelper(DatItem item, bool clean, bool remUnicode)
{
string key = "";
// If there's no name in the rom, we log and skip it
if (item.Name == null)
{
Globals.Logger.Warning("{0}: Rom with no name found! Skipping...", FileName);
return key;
}
// If the name ends with a directory separator, we log and skip it (DOSCenter only?)
if (item.Name.EndsWith("/") || item.Name.EndsWith("\\"))
{
Globals.Logger.Warning("{0}: Rom ending with directory separator found: '{1}'. Skipping...", FileName, item.Name);
return key;
}
// If we're in cleaning mode, sanitize the game name
item.MachineName = (clean ? Style.CleanGameName(item.MachineName) : item.MachineName);
// If we're stripping unicode characters, do so from all relevant things
if (remUnicode)
{
item.Name = Style.RemoveUnicodeCharacters(item.Name);
item.MachineName = Style.RemoveUnicodeCharacters(item.MachineName);
item.MachineDescription = Style.RemoveUnicodeCharacters(item.MachineDescription);
}
// If we have a Rom or a Disk, clean the hash data
if (item.Type == ItemType.Rom)
{
Rom itemRom = (Rom)item;
// Sanitize the hashes from null, hex sizes, and "true blank" strings
itemRom.CRC = Style.CleanHashData(itemRom.CRC, Constants.CRCLength);
itemRom.MD5 = Style.CleanHashData(itemRom.MD5, Constants.MD5Length);
itemRom.SHA1 = Style.CleanHashData(itemRom.SHA1, Constants.SHA1Length);
itemRom.SHA256 = Style.CleanHashData(itemRom.SHA256, Constants.SHA256Length);
itemRom.SHA384 = Style.CleanHashData(itemRom.SHA384, Constants.SHA384Length);
itemRom.SHA512 = Style.CleanHashData(itemRom.SHA512, Constants.SHA512Length);
// 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
if ((itemRom.Size == 0 || itemRom.Size == -1)
&& ((itemRom.CRC == Constants.CRCZero || String.IsNullOrEmpty(itemRom.CRC))
|| itemRom.MD5 == Constants.MD5Zero
|| itemRom.SHA1 == Constants.SHA1Zero
|| itemRom.SHA256 == Constants.SHA256Zero
|| itemRom.SHA384 == Constants.SHA384Zero
|| itemRom.SHA512 == Constants.SHA512Zero))
{
// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually
itemRom.Size = Constants.SizeZero;
itemRom.CRC = Constants.CRCZero;
itemRom.MD5 = Constants.MD5Zero;
itemRom.SHA1 = Constants.SHA1Zero;
itemRom.SHA256 = null;
itemRom.SHA384 = null;
itemRom.SHA512 = null;
//itemRom.SHA256 = Constants.SHA256Zero;
//itemRom.SHA384 = Constants.SHA384Zero;
//itemRom.SHA512 = Constants.SHA512Zero;
}
// 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("{0}: Incomplete entry for '{1}' will be output as nodump", FileName, itemRom.Name);
itemRom.ItemStatus = ItemStatus.Nodump;
}
// If the file has a size but aboslutely no hashes, skip and log
else if (itemRom.ItemStatus != ItemStatus.Nodump
&& itemRom.Size > 0
&& String.IsNullOrEmpty(itemRom.CRC)
&& String.IsNullOrEmpty(itemRom.MD5)
&& String.IsNullOrEmpty(itemRom.SHA1)
&& String.IsNullOrEmpty(itemRom.SHA256)
&& String.IsNullOrEmpty(itemRom.SHA384)
&& String.IsNullOrEmpty(itemRom.SHA512))
{
Globals.Logger.Verbose("{0}: Incomplete entry for '{1}' will be output as nodump", FileName, itemRom.Name);
itemRom.ItemStatus = ItemStatus.Nodump;
}
item = itemRom;
}
else if (item.Type == ItemType.Disk)
{
Disk itemDisk = (Disk)item;
// Sanitize the hashes from null, hex sizes, and "true blank" strings
itemDisk.MD5 = Style.CleanHashData(itemDisk.MD5, Constants.MD5Length);
itemDisk.SHA1 = Style.CleanHashData(itemDisk.SHA1, Constants.SHA1Length);
itemDisk.SHA256 = Style.CleanHashData(itemDisk.SHA256, Constants.SHA256Length);
itemDisk.SHA384 = Style.CleanHashData(itemDisk.SHA384, Constants.SHA384Length);
itemDisk.SHA512 = Style.CleanHashData(itemDisk.SHA512, Constants.SHA512Length);
// If the file has aboslutely no hashes, skip and log
if (itemDisk.ItemStatus != ItemStatus.Nodump
&& String.IsNullOrEmpty(itemDisk.MD5)
&& String.IsNullOrEmpty(itemDisk.SHA1)
&& String.IsNullOrEmpty(itemDisk.SHA256)
&& String.IsNullOrEmpty(itemDisk.SHA384)
&& String.IsNullOrEmpty(itemDisk.SHA512))
{
Globals.Logger.Verbose("Incomplete entry for '{0}' will be output as nodump", itemDisk.Name);
itemDisk.ItemStatus = ItemStatus.Nodump;
}
item = itemDisk;
}
// Get the key and add statistical data
switch (item.Type)
{
case ItemType.Archive:
case ItemType.BiosSet:
case ItemType.Release:
case ItemType.Sample:
key = item.Type.ToString();
break;
case ItemType.Disk:
key = ((Disk)item).MD5;
break;
case ItemType.Rom:
key = ((Rom)item).Size + "-" + ((Rom)item).CRC;
break;
default:
key = "default";
break;
}
// Add the item to the DAT
Add(key, item);
return key;
}
/// <summary>
/// Add a rom to the Dat after checking
/// </summary>
/// <param name="item">Item data to check against</param>
/// <param name="clean">True if the names should be cleaned to WoD standards, false otherwise</param>
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
/// <returns>The key for the item</returns>
public async Task<string> ParseAddHelperAsync(DatItem item, bool clean, bool remUnicode)
{
return await Task.Run(() => ParseAddHelper(item, clean, remUnicode));
}
#endregion
#region Populate DAT from Directory
/// <summary>
/// Create a new Dat from a directory
/// </summary>
/// <param name="basePath">Base folder to be used in creating the DAT</param>
/// <param name="omitFromScan">Hash flag saying what hashes should not be calculated</param>
/// <param name="bare">True if the date should be omitted from the DAT, false otherwise</param>
/// <param name="archivesAsFiles">True if archives should be treated as files, false otherwise</param>
/// <param name="skipFileType">Type of files that should be skipped</param>
/// <param name="addBlanks">True if blank items should be created for empty folders, false otherwise</param>
/// <param name="addDate">True if dates should be archived for all files, false otherwise</param>
/// <param name="tempDir">Name of the directory to create a temp folder in (blank is current directory)</param>
/// <param name="outDir">Output directory to </param>
/// <param name="copyFiles">True if files should be copied to the temp directory before hashing, false otherwise</param>
/// <param name="headerToCheckAgainst">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
/// <param name="chdsAsFiles">True if CHDs should be treated like regular files, false otherwise</param>
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)
{
// If the description is defined but not the name, set the name from the description
if (String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description))
{
Name = Description;
}
// If the name is defined but not the description, set the description from the name
else if (!String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description))
{
Description = Name + (bare ? "" : " (" + Date + ")");
}
// If neither the name or description are defined, set them from the automatic values
else if (String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description))
{
Name = basePath.Split(Path.DirectorySeparatorChar).Last();
Description = Name + (bare ? "" : " (" + Date + ")");
}
// Process the input
if (Directory.Exists(basePath))
{
Globals.Logger.Verbose("Folder found: {0}", basePath);
// Process the files in the main folder
List<string> files = Directory.EnumerateFiles(basePath, "*", SearchOption.TopDirectoryOnly).ToList();
Parallel.ForEach(files, Globals.ParallelOptions, item =>
{
CheckFileForHashes(item, basePath, omitFromScan, bare, archivesAsFiles, skipFileType,
addBlanks, addDate, tempDir, copyFiles, headerToCheckAgainst, chdsAsFiles);
});
// Find all top-level subfolders
files = Directory.EnumerateDirectories(basePath, "*", SearchOption.TopDirectoryOnly).ToList();
foreach (string item in files)
{
List<string> subfiles = Directory.EnumerateFiles(item, "*", SearchOption.AllDirectories).ToList();
Parallel.ForEach(subfiles, Globals.ParallelOptions, subitem =>
{
CheckFileForHashes(subitem, basePath, omitFromScan, bare, archivesAsFiles, skipFileType,
addBlanks, addDate, tempDir, copyFiles, headerToCheckAgainst, chdsAsFiles);
});
}
// Now find all folders that are empty, if we are supposed to
if (!Romba && addBlanks)
{
List<string> empties = FileTools.GetEmptyDirectories(basePath).ToList();
Parallel.ForEach(empties, Globals.ParallelOptions, dir =>
{
// Get the full path for the directory
string fulldir = Path.GetFullPath(dir);
// Set the temporary variables
string gamename = "";
string romname = "";
// 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")
{
gamename = fulldir.Remove(0, basePath.Length + 1);
romname = "_";
}
// Otherwise, we want just the top level folder as the game, and the file as everything else
else
{
gamename = fulldir.Remove(0, basePath.Length + 1).Split(Path.DirectorySeparatorChar)[0];
romname = Path.Combine(fulldir.Remove(0, basePath.Length + 1 + gamename.Length), "_");
}
// Sanitize the names
if (gamename.StartsWith(Path.DirectorySeparatorChar.ToString()))
{
gamename = gamename.Substring(1);
}
if (gamename.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
gamename = gamename.Substring(0, gamename.Length - 1);
}
if (romname.StartsWith(Path.DirectorySeparatorChar.ToString()))
{
romname = romname.Substring(1);
}
if (romname.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
romname = romname.Substring(0, romname.Length - 1);
}
Globals.Logger.Verbose("Adding blank empty folder: {0}", gamename);
this["null"].Add(new Rom(romname, gamename, omitFromScan));
});
}
}
else if (File.Exists(basePath))
{
CheckFileForHashes(basePath, Path.GetDirectoryName(Path.GetDirectoryName(basePath)), omitFromScan, bare, archivesAsFiles,
skipFileType, addBlanks, addDate, tempDir, copyFiles, headerToCheckAgainst, chdsAsFiles);
}
// 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())
{
FileTools.TryDeleteDirectory(tempDir);
}
return true;
}
/// <summary>
/// Check a given file for hashes, based on current settings
/// </summary>
/// <param name="item">Filename of the item to be checked</param>
/// <param name="basePath">Base folder to be used in creating the DAT</param>
/// <param name="omitFromScan">Hash flag saying what hashes should not be calculated</param>
/// <param name="bare">True if the date should be omitted from the DAT, false otherwise</param>
/// <param name="archivesAsFiles">True if archives should be treated as files, false otherwise</param>
/// <param name="skipFileType">Type of files that should be skipped</param>
/// <param name="addBlanks">True if blank items should be created for empty folders, false otherwise</param>
/// <param name="addDate">True if dates should be archived for all files, false otherwise</param>
/// <param name="tempDir">Name of the directory to create a temp folder in (blank is current directory)</param>
/// <param name="copyFiles">True if files should be copied to the temp directory before hashing, false otherwise</param>
/// <param name="headerToCheckAgainst">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
/// <param name="chdsAsFiles">True if CHDs should be treated like regular files, false otherwise</param>
private void CheckFileForHashes(string item, string basePath, Hash omitFromScan, bool bare, bool archivesAsFiles,
SkipFileType skipFileType, bool addBlanks, bool addDate, string tempDir, bool copyFiles, string headerToCheckAgainst, bool chdsAsFiles)
{
// Define the temporary directory
string tempSubDir = Path.GetFullPath(Path.Combine(tempDir, Path.GetRandomFileName())) + Path.DirectorySeparatorChar;
// Special case for if we are in Romba mode (all names are supposed to be SHA-1 hashes)
if (Romba)
{
Rom rom = ArchiveTools.GetTorrentGZFileInfo(item);
// If the rom is valid, write it out
if (rom != null && rom.Name != null)
{
// Add the list if it doesn't exist already
Add(rom.Size + "-" + rom.CRC, rom);
Globals.Logger.User("File added: {0}", Path.GetFileNameWithoutExtension(item) + Environment.NewLine);
}
else
{
Globals.Logger.User("File not added: {0}", Path.GetFileNameWithoutExtension(item) + Environment.NewLine);
return;
}
return;
}
// If we're copying files, copy it first and get the new filename
string newItem = item;
string newBasePath = basePath;
if (copyFiles)
{
newBasePath = Path.Combine(tempDir, Path.GetRandomFileName());
newItem = Path.GetFullPath(Path.Combine(newBasePath, Path.GetFullPath(item).Remove(0, basePath.Length + 1)));
Directory.CreateDirectory(Path.GetDirectoryName(newItem));
File.Copy(item, newItem, true);
}
// Create a list for all found items
List<Rom> extracted = null;
// If we don't have archives as files, try to scan the file as an archive
if (!archivesAsFiles)
{
// Get the base archive first
BaseArchive archive = ArchiveTools.CreateArchiveFromExistingInput(newItem);
// Now get all extracted items from the archive
if (archive != null)
{
extracted = archive.GetArchiveFileInfo(omitFromScan: omitFromScan, date: addDate);
}
}
// If the file should be skipped based on type, do so now
if ((extracted != null && skipFileType == SkipFileType.Archive)
|| (extracted == null && skipFileType == SkipFileType.File))
{
return;
}
// If the extracted list is null, just scan the item itself
if (extracted == null || archivesAsFiles)
{
ProcessFile(newItem, "", newBasePath, omitFromScan, addDate, headerToCheckAgainst, chdsAsFiles);
}
// Otherwise, add all of the found items
else
{
// First take care of the found items
Parallel.ForEach(extracted, Globals.ParallelOptions, rom =>
{
ProcessFileHelper(newItem,
rom,
basePath,
(Path.GetDirectoryName(Path.GetFullPath(item)) + Path.DirectorySeparatorChar).Remove(0, basePath.Length) + Path.GetFileNameWithoutExtension(item));
});
// Then, if we're looking for blanks, get all of the blank folders and add them
if (addBlanks)
{
List<string> empties = new List<string>();
// Get the base archive first
BaseArchive archive = ArchiveTools.CreateArchiveFromExistingInput(newItem);
// Now get all blank folders from the archive
if (archive != null)
{
empties = archive.GetEmptyFolders();
}
// 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);
ProcessFileHelper(newItem,
emptyRom,
basePath,
(Path.GetDirectoryName(Path.GetFullPath(item)) + Path.DirectorySeparatorChar).Remove(0, basePath.Length) + Path.GetFileNameWithoutExtension(item));
});
}
}
// Cue to delete the file if it's a copy
if (copyFiles && item != newItem)
{
FileTools.TryDeleteDirectory(newBasePath);
}
// Delete the sub temp directory
FileTools.TryDeleteDirectory(tempSubDir);
}
/// <summary>
/// Process a single file as a file
/// </summary>
/// <param name="item">File to be added</param>
/// <param name="parent">Parent game to be used</param>
/// <param name="basePath">Path the represents the parent directory</param>
/// <param name="omitFromScan">Hash flag saying what hashes should not be calculated</param>
/// <param name="addDate">True if dates should be archived for all files, false otherwise</param>
/// <param name="headerToCheckAgainst">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
/// <param name="chdsAsFiles">True if CHDs should be treated like regular files, false otherwise</param>
private void ProcessFile(string item, string parent, string basePath, Hash omitFromScan,
bool addDate, string headerToCheckAgainst, bool chdsAsFiles)
{
Globals.Logger.Verbose("'{0}' treated like a file", Path.GetFileName(item));
DatItem datItem = FileTools.GetFileInfo(item, omitFromScan: omitFromScan, date: addDate, header: headerToCheckAgainst, chdsAsFiles: chdsAsFiles);
ProcessFileHelper(item, datItem, basePath, parent);
}
/// <summary>
/// Process a single file as a file (with found Rom data)
/// </summary>
/// <param name="item">File to be added</param>
/// <param name="item">Rom data to be used to write to file</param>
/// <param name="basepath">Path the represents the parent directory</param>
/// <param name="parent">Parent game to be used</param>
private void ProcessFileHelper(string item, DatItem datItem, string basepath, string parent)
{
// If the datItem isn't a Rom or Disk, return
if (datItem.Type != ItemType.Rom && datItem.Type != ItemType.Disk)
{
return;
}
string key = "";
if (datItem.Type == ItemType.Rom)
{
key = ((Rom)datItem).Size + "-" + ((Rom)datItem).CRC;
}
else if (datItem.Type == ItemType.Disk)
{
key = ((Disk)datItem).SHA1;
}
// Add the list if it doesn't exist already
Add(key);
try
{
// If the basepath ends with a directory separator, remove it
if (!basepath.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
basepath += Path.DirectorySeparatorChar.ToString();
}
// Make sure we have the full item path
item = Path.GetFullPath(item);
// Get the data to be added as game and item names
string gamename = "";
string romname = "";
// If the parent is blank, then we have a non-archive file
if (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")
{
gamename = Path.GetDirectoryName(item.Remove(0, basepath.Length));
romname = Path.GetFileName(item);
}
// Otherwise, we want just the top level folder as the game, and the file as everything else
else
{
gamename = item.Remove(0, basepath.Length).Split(Path.DirectorySeparatorChar)[0];
romname = item.Remove(0, (Path.Combine(basepath, gamename).Length));
}
}
// Otherwise, we assume that we have an archive
else
{
// If we have a SuperDAT, we want the archive name as the game, and the file as everything else (?)
if (Type == "SuperDAT")
{
gamename = parent;
romname = datItem.Name;
}
// Otherwise, we want the archive name as the game, and the file as everything else
else
{
gamename = parent;
romname = datItem.Name;
}
}
// Sanitize the names
if (romname == null)
{
romname = "";
}
if (gamename.StartsWith(Path.DirectorySeparatorChar.ToString()))
{
gamename = gamename.Substring(1);
}
if (gamename.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
gamename = gamename.Substring(0, gamename.Length - 1);
}
if (romname.StartsWith(Path.DirectorySeparatorChar.ToString()))
{
romname = romname.Substring(1);
}
if (romname.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
romname = romname.Substring(0, romname.Length - 1);
}
if (!String.IsNullOrEmpty(gamename) && String.IsNullOrEmpty(romname))
{
romname = gamename;
gamename = "Default";
}
// Update rom information
datItem.Name = romname;
datItem.MachineName = gamename;
datItem.MachineDescription = gamename;
// If we have a Disk, then the ".chd" extension needs to be removed
if (datItem.Type == ItemType.Disk)
{
datItem.Name = datItem.Name.Replace(".chd", "");
}
// Add the file information to the DAT
Add(key, datItem);
Globals.Logger.User("File added: {0}", romname + Environment.NewLine);
}
catch (IOException ex)
{
Globals.Logger.Error(ex.ToString());
return;
}
}
#endregion
#region Rebuilding and Verifying
/// <summary>
/// Process the DAT and find all matches in input files and folders assuming they're a depot
/// </summary>
/// <param name="inputs">List of input files/folders to check</param>
/// <param name="outDir">Output directory to use to build to</param>
/// <param name="date">True if the date from the DAT should be used if available, false otherwise</param>
/// <param name="delete">True if input files should be deleted, false otherwise</param>
/// <param name="inverse">True if the DAT should be used as a filter instead of a template, false otherwise</param>
/// <param name="outputFormat">Output format that files should be written to</param>
/// <param name="romba">True if files should be output in Romba depot folders, false otherwise</param>
/// <param name="updateDat">True if the updated DAT should be output, false otherwise</param>
/// <param name="headerToCheckAgainst">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
/// <returns>True if rebuilding was a success, false otherwise</returns>
public bool RebuildDepot(List<string> inputs, string outDir, bool date, bool delete,
bool inverse, OutputFormat outputFormat, bool romba, bool updateDat, string headerToCheckAgainst)
{
#region Perform setup
// If the DAT is not populated and inverse is not set, inform the user and quit
if (Count == 0 && !inverse)
{
Globals.Logger.User("No entries were found to rebuild, exiting...");
return false;
}
// Check that the output directory exists
if (!Directory.Exists(outDir))
{
Directory.CreateDirectory(outDir);
outDir = Path.GetFullPath(outDir);
}
// Now we want to get forcepack flag if it's not overridden
if (outputFormat == OutputFormat.Folder && ForcePacking != ForcePacking.None)
{
switch (ForcePacking)
{
case ForcePacking.Zip:
outputFormat = OutputFormat.TorrentZip;
break;
case ForcePacking.Unzip:
outputFormat = OutputFormat.Folder;
break;
}
}
// Preload the Skipper list
int listcount = Skipper.List.Count;
#endregion
bool success = true;
#region Rebuild from depots in order
string format = "";
switch (outputFormat)
{
case OutputFormat.Folder:
format = "directory";
break;
case OutputFormat.TapeArchive:
format = "TAR";
break;
case OutputFormat.Torrent7Zip:
format = "Torrent7Z";
break;
case OutputFormat.TorrentGzip:
format = "TorrentGZ";
break;
case OutputFormat.TorrentLrzip:
format = "TorrentLRZ";
break;
case OutputFormat.TorrentRar:
format = "TorrentRAR";
break;
case OutputFormat.TorrentXZ:
format = "TorrentXZ";
break;
case OutputFormat.TorrentZip:
format = "TorrentZip";
break;
}
InternalStopwatch watch = new InternalStopwatch("Rebuilding all files to {0}", format);
// Now loop through and get only directories from the input paths
List<string> directories = new List<string>();
Parallel.ForEach(inputs, Globals.ParallelOptions, input =>
{
// Add to the list if the input is a directory
if (Directory.Exists(input))
{
Globals.Logger.Verbose("Adding depot: {0}", input);
lock (directories)
{
directories.Add(input);
}
}
});
// If we don't have any directories, we want to exit
if (directories.Count == 0)
{
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);
// Then we want to loop through each of the hashes and see if we can rebuild
List<string> hashes = Keys;
foreach (string hash in hashes)
{
// Pre-empt any issues that could arise from string length
if (hash.Length != Constants.SHA1Length)
{
continue;
}
Globals.Logger.User("Checking hash '{0}'", hash);
// Get the extension path for the hash
string subpath = Style.GetRombaPath(hash);
// Find the first depot that includes the hash
string foundpath = null;
foreach (string directory in directories)
{
if (File.Exists(Path.Combine(directory, subpath)))
{
foundpath = Path.Combine(directory, subpath);
break;
}
}
// If we didn't find a path, then we continue
if (foundpath == null)
{
continue;
}
// If we have a path, we want to try to get the rom information
Rom fileinfo = ArchiveTools.GetTorrentGZFileInfo(foundpath);
// If the file information is null, then we continue
if (fileinfo == null)
{
continue;
}
// Otherwise, we rebuild that file to all locations that we need to
RebuildIndividualFile(fileinfo, foundpath, outDir, date, inverse, outputFormat, romba,
updateDat, false /* isZip */, headerToCheckAgainst);
}
watch.Stop();
#endregion
// If we're updating the DAT, output to the rebuild directory
if (updateDat)
{
FileName = "fixDAT_" + FileName;
Name = "fixDAT_" + Name;
Description = "fixDAT_" + Description;
RemoveMarkedItems();
WriteToFile(outDir);
}
return success;
}
/// <summary>
/// Process the DAT and find all matches in input files and folders
/// </summary>
/// <param name="inputs">List of input files/folders to check</param>
/// <param name="outDir">Output directory to use to build to</param>
/// <param name="quickScan">True to enable external scanning of archives, false otherwise</param>
/// <param name="date">True if the date from the DAT should be used if available, false otherwise</param>
/// <param name="delete">True if input files should be deleted, false otherwise</param>
/// <param name="inverse">True if the DAT should be used as a filter instead of a template, false otherwise</param>
/// <param name="outputFormat">Output format that files should be written to</param>
/// <param name="romba">True if files should be output in Romba depot folders, false otherwise</param>
/// <param name="archiveScanLevel">ArchiveScanLevel representing the archive handling levels</param>
/// <param name="updateDat">True if the updated DAT should be output, false otherwise</param>
/// <param name="headerToCheckAgainst">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
/// <param name="chdsAsFiles">True if CHDs should be treated like regular files, false otherwise</param>
/// <returns>True if rebuilding was a success, false otherwise</returns>
public bool RebuildGeneric(List<string> inputs, string outDir, bool quickScan, bool date,
bool delete, bool inverse, OutputFormat outputFormat, bool romba, ArchiveScanLevel archiveScanLevel, bool updateDat,
string headerToCheckAgainst, bool chdsAsFiles)
{
#region Perform setup
// If the DAT is not populated and inverse is not set, inform the user and quit
if (Count == 0 && !inverse)
{
Globals.Logger.User("No entries were found to rebuild, exiting...");
return false;
}
// Check that the output directory exists
if (!Directory.Exists(outDir))
{
Directory.CreateDirectory(outDir);
outDir = Path.GetFullPath(outDir);
}
// Now we want to get forcepack flag if it's not overridden
if (outputFormat == OutputFormat.Folder && ForcePacking != ForcePacking.None)
{
switch (ForcePacking)
{
case ForcePacking.Zip:
outputFormat = OutputFormat.TorrentZip;
break;
case ForcePacking.Unzip:
outputFormat = OutputFormat.Folder;
break;
}
}
// Preload the Skipper list
int listcount = Skipper.List.Count;
#endregion
bool success = true;
#region Rebuild from sources in order
string format = "";
switch (outputFormat)
{
case OutputFormat.Folder:
format = "directory";
break;
case OutputFormat.TapeArchive:
format = "TAR";
break;
case OutputFormat.Torrent7Zip:
format = "Torrent7Z";
break;
case OutputFormat.TorrentGzip:
format = "TorrentGZ";
break;
case OutputFormat.TorrentLrzip:
format = "TorrentLRZ";
break;
case OutputFormat.TorrentRar:
format = "TorrentRAR";
break;
case OutputFormat.TorrentXZ:
format = "TorrentXZ";
break;
case OutputFormat.TorrentZip:
format = "TorrentZip";
break;
}
InternalStopwatch watch = new InternalStopwatch("Rebuilding all files to {0}", format);
// Now loop through all of the files in all of the inputs
foreach (string input in inputs)
{
// If the input is a file
if (File.Exists(input))
{
Globals.Logger.User("Checking file: {0}", input);
RebuildGenericHelper(input, outDir, quickScan, date, delete, inverse,
outputFormat, romba, archiveScanLevel, updateDat, headerToCheckAgainst, chdsAsFiles);
}
// If the input is a directory
else if (Directory.Exists(input))
{
Globals.Logger.Verbose("Checking directory: {0}", input);
foreach (string file in Directory.EnumerateFiles(input, "*", SearchOption.AllDirectories))
{
Globals.Logger.User("Checking file: {0}", file);
RebuildGenericHelper(file, outDir, quickScan, date, delete, inverse,
outputFormat, romba, archiveScanLevel, updateDat, headerToCheckAgainst, chdsAsFiles);
}
}
}
watch.Stop();
#endregion
// If we're updating the DAT, output to the rebuild directory
if (updateDat)
{
FileName = "fixDAT_" + FileName;
Name = "fixDAT_" + Name;
Description = "fixDAT_" + Description;
RemoveMarkedItems();
WriteToFile(outDir);
}
return success;
}
/// <summary>
/// Attempt to add a file to the output if it matches
/// </summary>
/// <param name="file">Name of the file to process</param>
/// <param name="outDir">Output directory to use to build to</param>
/// <param name="quickScan">True to enable external scanning of archives, false otherwise</param>
/// <param name="date">True if the date from the DAT should be used if available, false otherwise</param>
/// <param name="delete">True if input files should be deleted, false otherwise</param>
/// <param name="inverse">True if the DAT should be used as a filter instead of a template, false otherwise</param>
/// <param name="outputFormat">Output format that files should be written to</param>
/// <param name="romba">True if files should be output in Romba depot folders, false otherwise</param>
/// <param name="archiveScanLevel">ArchiveScanLevel representing the archive handling levels</param>
/// <param name="updateDat">True if the updated DAT should be output, false otherwise</param>
/// <param name="headerToCheckAgainst">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
/// <param name="chdsAsFiles">True if CHDs should be treated like regular files, false otherwise</param>
private void RebuildGenericHelper(string file, string outDir, bool quickScan, bool date,
bool delete, bool inverse, OutputFormat outputFormat, bool romba, ArchiveScanLevel archiveScanLevel, bool updateDat,
string headerToCheckAgainst, bool chdsAsFiles)
{
// If we somehow have a null filename, return
if (file == null)
{
return;
}
// Set the deletion variables
bool usedExternally = false;
bool usedInternally = false;
// Get the required scanning level for the file
ArchiveTools.GetInternalExternalProcess(file, archiveScanLevel, out bool shouldExternalProcess, out bool shouldInternalProcess);
// If we're supposed to scan the file externally
if (shouldExternalProcess)
{
// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually
DatItem fileinfo = FileTools.GetFileInfo(file, omitFromScan: (quickScan ? Hash.SecureHashes : Hash.DeepHashes),
header: headerToCheckAgainst, chdsAsFiles: chdsAsFiles);
usedExternally = RebuildIndividualFile(fileinfo, file, outDir, date, inverse, outputFormat,
romba, updateDat, null /* isZip */, headerToCheckAgainst);
}
// If we're supposed to scan the file internally
if (shouldInternalProcess)
{
// Create an empty list of Roms for archive entries
List<Rom> entries = null;
usedInternally = true;
// Get the TGZ status for later
bool isTorrentGzip = (ArchiveTools.GetTorrentGZFileInfo(file) != null);
// Get the base archive first
BaseArchive archive = ArchiveTools.CreateArchiveFromExistingInput(file);
// Now get all extracted items from the archive
if (archive != null)
{
// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually
entries = archive.GetArchiveFileInfo(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
DatItem fileinfo = FileTools.GetFileInfo(file, omitFromScan: (quickScan ? Hash.SecureHashes : Hash.DeepHashes), chdsAsFiles: chdsAsFiles);
usedExternally = RebuildIndividualFile(fileinfo, file, outDir, date, inverse, outputFormat,
romba, updateDat, null /* isZip */, headerToCheckAgainst);
}
// Otherwise, loop through the entries and try to match
else
{
foreach (Rom entry in entries)
{
usedInternally &= RebuildIndividualFile(entry, file, outDir, date, inverse, outputFormat,
romba, updateDat, !isTorrentGzip /* isZip */, headerToCheckAgainst);
}
}
}
// If we are supposed to delete the file, do so
if (delete && (usedExternally || usedInternally))
{
FileTools.TryDeleteFile(file);
}
}
/// <summary>
/// Find duplicates and rebuild individual files to output
/// </summary>
/// <param name="datItem">Information for the current file to rebuild from</param>
/// <param name="file">Name of the file to process</param>
/// <param name="outDir">Output directory to use to build to</param>
/// <param name="date">True if the date from the DAT should be used if available, false otherwise</param>
/// <param name="inverse">True if the DAT should be used as a filter instead of a template, false otherwise</param>
/// <param name="outputFormat">Output format that files should be written to</param>
/// <param name="romba">True if files should be output in Romba depot folders, false otherwise</param>
/// <param name="updateDat">True if the updated DAT should be output, false otherwise</param>
/// <param name="isZip">True if the input file is an archive, false if the file is TGZ, null otherwise</param>
/// <param name="headerToCheckAgainst">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
/// <returns>True if the file was able to be rebuilt, false otherwise</returns>
private bool RebuildIndividualFile(DatItem datItem, string file, string outDir, bool date,
bool inverse, OutputFormat outputFormat, bool romba, bool updateDat, bool? isZip, string headerToCheckAgainst)
{
// Set the output value
bool rebuilt = false;
// If the DatItem is a Disk, force rebuilding to a folder except if TGZ
if (datItem.Type == ItemType.Disk && outputFormat != OutputFormat.TorrentGzip)
{
outputFormat = OutputFormat.Folder;
}
// Prepopluate a few key strings based on DatItem type
string crc = null;
string sha1 = null;
if (datItem.Type == ItemType.Rom)
{
crc = ((Rom)datItem).CRC;
sha1 = ((Rom)datItem).SHA1;
}
else if (datItem.Type == ItemType.Disk)
{
crc = "";
sha1 = ((Disk)datItem).SHA1;
}
// Find if the file has duplicates in the DAT
bool hasDuplicates = datItem.HasDuplicates(this);
// If it has duplicates and we're not filtering, rebuild it
if (hasDuplicates && !inverse)
{
// Get the list of duplicates to rebuild to
List<DatItem> dupes = datItem.GetDuplicates(this, remove: updateDat);
// If we don't have any duplicates, continue
if (dupes.Count == 0)
{
return rebuilt;
}
// If we have a very specifc TGZ->TGZ case, just copy it accordingly
if (isZip == false && ArchiveTools.GetTorrentGZFileInfo(file) != null && outputFormat == OutputFormat.TorrentGzip)
{
// Get the proper output path
if (romba)
{
outDir = Path.Combine(outDir, Style.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);
rebuilt &= true;
}
catch
{
rebuilt = false;
}
return rebuilt;
}
// 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 = ArchiveTools.CreateArchiveFromExistingInput(file);
if (archive != null)
{
(fileStream, realName) = archive.ExtractEntryStream(datItem.Name);
}
}
// Otherwise, just open the filestream
else
{
fileStream = FileTools.TryOpenRead(file);
}
// If the stream is null, then continue
if (fileStream == null)
{
return rebuilt;
}
// Seek to the beginning of the stream
fileStream.Seek(0, SeekOrigin.Begin);
Globals.Logger.User("Matches found for '{0}', rebuilding accordingly...", Style.GetFileName(datItem.Name));
rebuilt = true;
// Now loop through the list and rebuild accordingly
foreach (DatItem item in dupes)
{
// Get the output archive, if possible
BaseArchive outputArchive = ArchiveTools.CreateArchiveFromOutputFormat(outputFormat);
// Now rebuild to the output file
outputArchive.Write(fileStream, outDir, (Rom)item, date: date, romba: romba);
}
// 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 specifc TGZ->TGZ case, just copy it accordingly
if (isZip == false && ArchiveTools.GetTorrentGZFileInfo(file) != null && outputFormat == OutputFormat.TorrentGzip)
{
// Get the proper output path
if (romba)
{
outDir = Path.Combine(outDir, Style.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);
rebuilt &= true;
}
catch
{
rebuilt = false;
}
return rebuilt;
}
// 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 = ArchiveTools.CreateArchiveFromExistingInput(file);
if (archive != null)
{
(fileStream, realName) = archive.ExtractEntryStream(datItem.Name);
}
}
// Otherwise, just open the filestream
else
{
fileStream = FileTools.TryOpenRead(file);
}
// If the stream is null, then continue
if (fileStream == null)
{
return rebuilt;
}
// Get the item from the current file
Rom item = (Rom)FileTools.GetStreamInfo(fileStream, fileStream.Length, keepReadOpen: true);
item.MachineName = Style.GetFileNameWithoutExtension(item.Name);
item.MachineDescription = Style.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 '{0}', rebuilding accordingly from inverse flag...", Style.GetFileName(datItem.Name));
// Get the output archive, if possible
BaseArchive outputArchive = ArchiveTools.CreateArchiveFromOutputFormat(outputFormat);
// Now rebuild to the output file
if (outputArchive == null)
{
string outfile = Path.Combine(outDir, Style.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 = FileTools.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.IsNullOrEmpty(item.Date))
{
File.SetCreationTime(outfile, DateTime.Parse(item.Date));
}
rebuilt &= true;
}
catch
{
rebuilt &= false;
}
}
else
{
rebuilt &= outputArchive.Write(fileStream, outDir, item, date: date, romba: romba);
}
// Close the input stream
fileStream?.Dispose();
}
// Now we want to take care of headers, if applicable
if (headerToCheckAgainst != null)
{
// 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 = ArchiveTools.CreateArchiveFromExistingInput(file);
if (archive != null)
{
(fileStream, realName) = archive.ExtractEntryStream(datItem.Name);
}
}
// Otherwise, just open the filestream
else
{
fileStream = FileTools.TryOpenRead(file);
}
// If the stream is null, then continue
if (fileStream == null)
{
return rebuilt;
}
// Check to see if we have a matching header first
SkipperRule rule = Skipper.GetMatchingRule(fileStream, Path.GetFileNameWithoutExtension(headerToCheckAgainst));
// If there's a match, create the new file to write
if (rule.Tests != null && rule.Tests.Count != 0)
{
// If the file could be transformed correctly
MemoryStream transformStream = new MemoryStream();
if (rule.TransformStream(fileStream, transformStream, keepReadOpen: true, keepWriteOpen: true))
{
// Get the file informations that we will be using
Rom headerless = (Rom)FileTools.GetStreamInfo(transformStream, transformStream.Length, keepReadOpen: true);
// Find if the file has duplicates in the DAT
hasDuplicates = headerless.HasDuplicates(this);
// If it has duplicates and we're not filtering, rebuild it
if (hasDuplicates && !inverse)
{
// Get the list of duplicates to rebuild to
List<DatItem> dupes = headerless.GetDuplicates(this, remove: updateDat);
// If we don't have any duplicates, continue
if (dupes.Count == 0)
{
return rebuilt;
}
Globals.Logger.User("Headerless matches found for '{0}', rebuilding accordingly...", Style.GetFileName(datItem.Name));
rebuilt = true;
// Now loop through the list and rebuild accordingly
foreach (DatItem item in dupes)
{
// Create a headered item to use as well
datItem.CopyMachineInformation(item);
datItem.Name += "_" + crc;
// If either copy succeeds, then we want to set rebuilt to true
bool eitherSuccess = false;
// Get the output archive, if possible
BaseArchive outputArchive = ArchiveTools.CreateArchiveFromOutputFormat(outputFormat);
// Now rebuild to the output file
eitherSuccess |= outputArchive.Write(transformStream, outDir, (Rom)item, date: date, romba: romba);
eitherSuccess |= outputArchive.Write(fileStream, outDir, (Rom)datItem, date: date, romba: romba);
// Now add the success of either rebuild
rebuilt &= eitherSuccess;
}
}
}
// Dispose of the stream
transformStream?.Dispose();
}
// Dispose of the stream
fileStream?.Dispose();
}
return rebuilt;
}
/// <summary>
/// Process the DAT and verify from the depots
/// </summary>
/// <param name="inputs">List of input directories to compare against</param>
/// <param name="headerToCheckAgainst">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
/// <returns>True if verification was a success, false otherwise</returns>
public bool VerifyDepot(List<string> inputs, string headerToCheckAgainst)
{
bool success = true;
InternalStopwatch watch = new InternalStopwatch("Verifying all from supplied depots");
// Now loop through and get only directories from the input paths
List<string> directories = new List<string>();
foreach (string input in inputs)
{
// Add to the list if the input is a directory
if (Directory.Exists(input))
{
Globals.Logger.Verbose("Adding depot: {0}", input);
directories.Add(input);
}
}
// If we don't have any directories, we want to exit
if (directories.Count == 0)
{
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);
// Then we want to loop through each of the hashes and see if we can rebuild
List<string> hashes = Keys;
foreach (string hash in hashes)
{
// Pre-empt any issues that could arise from string length
if (hash.Length != Constants.SHA1Length)
{
continue;
}
Globals.Logger.User("Checking hash '{0}'", hash);
// Get the extension path for the hash
string subpath = Style.GetRombaPath(hash);
// Find the first depot that includes the hash
string foundpath = null;
foreach (string directory in directories)
{
if (File.Exists(Path.Combine(directory, subpath)))
{
foundpath = Path.Combine(directory, subpath);
break;
}
}
// If we didn't find a path, then we continue
if (foundpath == null)
{
continue;
}
// If we have a path, we want to try to get the rom information
Rom fileinfo = ArchiveTools.GetTorrentGZFileInfo(foundpath);
// If the file information is null, then we continue
if (fileinfo == null)
{
continue;
}
// Now we want to remove all duplicates from the DAT
fileinfo.GetDuplicates(this, 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;
WriteToFile(null);
return success;
}
/// <summary>
/// Process the DAT and verify the output directory
/// </summary>
/// <param name="inputs">List of input directories to compare against</param>
/// <param name="hashOnly">True if only hashes should be checked, false for full file information</param>
/// <param name="quickScan">True to enable external scanning of archives, false otherwise</param>
/// <param name="headerToCheckAgainst">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
/// <param name="chdsAsFiles">True if CHDs should be treated like regular files, false otherwise</param>
/// <returns>True if verification was a success, false otherwise</returns>
public bool VerifyGeneric(List<string> inputs, bool hashOnly, bool quickScan, string headerToCheckAgainst, bool chdsAsFiles)
{
// TODO: We want the cross section of what's the folder and what's in the DAT. Right now, it just has what's in the DAT that's not in the folder
bool success = true;
// Then, loop through and check each of the inputs
Globals.Logger.User("Processing files:\n");
foreach (string input in inputs)
{
// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually
PopulateFromDir(input, (quickScan ? Hash.SecureHashes : Hash.DeepHashes) /* omitFromScan */, true /* bare */, false /* archivesAsFiles */,
SkipFileType.None, false /* addBlanks */, false /* addDate */, "" /* tempDir */, false /* copyFiles */, headerToCheckAgainst, chdsAsFiles);
}
// Setup the fixdat
DatFile matched = new DatFile(this);
matched.ResetDictionary();
matched.FileName = "fixDat_" + matched.FileName;
matched.Name = "fixDat_" + matched.Name;
matched.Description = "fixDat_" + matched.Description;
matched.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);
// Then follow the same tactics as before
foreach (string key in Keys)
{
List<DatItem> roms = this[key];
foreach (DatItem rom in roms)
{
if (rom.SourceID == 99)
{
if (rom.Type == ItemType.Disk || rom.Type == ItemType.Rom)
{
matched.Add(((Disk)rom).SHA1, rom);
}
}
}
}
}
// If we are checking full names, get only files found in directory
else
{
foreach (string key in Keys)
{
List<DatItem> roms = this[key];
List<DatItem> newroms = DatItem.Merge(roms);
foreach (Rom rom in newroms)
{
if (rom.SourceID == 99)
{
matched.Add(rom.Size + "-" + rom.CRC, rom);
}
}
}
}
// Now output the fixdat to the main folder
success &= matched.WriteToFile("", stats: true);
return success;
}
#endregion
#region Splitting
/// <summary>
/// Split a DAT by input extensions
/// </summary>
/// <param name="outDir">Name of the directory to write the DATs out to</param>
/// <param name="basepath">Parent path for replacement</param>
/// <param name="extA">List of extensions to split on (first DAT)</param>
/// <param name="extB">List of extensions to split on (second DAT)</param>
/// <returns>True if split succeeded, false otherwise</returns>
public bool SplitByExtension(string outDir, string basepath, List<string> extA, List<string> extB)
{
// Make sure all of the extensions have a dot at the beginning
List<string> newExtA = new List<string>();
foreach (string s in extA)
{
newExtA.Add((s.StartsWith(".") ? s : "." + s).ToUpperInvariant());
}
string newExtAString = string.Join(",", newExtA);
List<string> newExtB = new List<string>();
foreach (string s in extB)
{
newExtB.Add((s.StartsWith(".") ? s : "." + s).ToUpperInvariant());
}
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,
};
// If roms is empty, return false
if (Count == 0)
{
return false;
}
// Now separate the roms accordingly
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> items = this[key];
foreach (DatItem item in items)
{
if (newExtA.Contains(Path.GetExtension(item.Name.ToUpperInvariant())))
{
datdataA.Add(key, item);
}
else if (newExtB.Contains(Path.GetExtension(item.Name.ToUpperInvariant())))
{
datdataB.Add(key, item);
}
else
{
datdataA.Add(key, item);
datdataB.Add(key, item);
}
}
});
// Get the output directory
if (outDir != "")
{
outDir = outDir + Path.GetDirectoryName(this.FileName).Remove(0, basepath.Length - 1);
}
else
{
outDir = Path.GetDirectoryName(this.FileName);
}
// Then write out both files
bool success = datdataA.WriteToFile(outDir);
success &= datdataB.WriteToFile(outDir);
return success;
}
/// <summary>
/// Split a DAT by best available hashes
/// </summary>
/// <param name="outDir">Name of the directory to write the DATs out to</param>
/// <param name="basepath">Parent path for replacement</param>
/// <returns>True if split succeeded, false otherwise</returns>
public bool SplitByHash(string outDir, string basepath)
{
// Sanitize the basepath to be more predictable
basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar);
// 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 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,
};
// Now populate each of the DAT objects in turn
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> items = this[key];
foreach (DatItem item in items)
{
// If the file is not a Rom or Disk, continue
if (item.Type != ItemType.Disk && item.Type != ItemType.Rom)
{
return;
}
// If the file is a nodump
if ((item.Type == ItemType.Rom && ((Rom)item).ItemStatus == ItemStatus.Nodump)
|| (item.Type == ItemType.Disk && ((Disk)item).ItemStatus == ItemStatus.Nodump))
{
nodump.Add(key, item);
}
// If the file has a SHA-512
else if ((item.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)item).SHA512))
|| (item.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)item).SHA512)))
{
sha512.Add(key, item);
}
// If the file has a SHA-384
else if ((item.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)item).SHA384))
|| (item.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)item).SHA384)))
{
sha384.Add(key, item);
}
// If the file has a SHA-256
else if ((item.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)item).SHA256))
|| (item.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)item).SHA256)))
{
sha256.Add(key, item);
}
// If the file has a SHA-1
else if ((item.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)item).SHA1))
|| (item.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)item).SHA1)))
{
sha1.Add(key, item);
}
// If the file has no SHA-1 but has an MD5
else if ((item.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)item).MD5))
|| (item.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)item).MD5)))
{
md5.Add(key, item);
}
// If the file has no MD5 but a CRC
else if ((item.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)item).SHA1))
|| (item.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)item).SHA1)))
{
crc.Add(key, item);
}
else
{
other.Add(key, item);
}
}
});
// Get the output directory
if (outDir != "")
{
outDir = outDir + Path.GetDirectoryName(this.FileName).Remove(0, basepath.Length - 1);
}
else
{
outDir = Path.GetDirectoryName(this.FileName);
}
// Now, output all of the files to the output directory
Globals.Logger.User("DAT information created, outputting new files");
bool success = true;
success &= nodump.WriteToFile(outDir);
success &= sha512.WriteToFile(outDir);
success &= sha384.WriteToFile(outDir);
success &= sha256.WriteToFile(outDir);
success &= sha1.WriteToFile(outDir);
success &= md5.WriteToFile(outDir);
success &= crc.WriteToFile(outDir);
return success;
}
/// <summary>
/// Split a SuperDAT by lowest available directory level
/// </summary>
/// <param name="outDir">Name of the directory to write the DATs out to</param>
/// <param name="basepath">Parent path for replacement</param>
/// <param name="shortname">True if short names should be used, false otherwise</param>
/// <param name="basedat">True if original filenames should be used as the base for output filename, false otherwise</param>
/// <returns>True if split succeeded, false otherwise</returns>
public bool SplitByLevel(string outDir, string basepath, bool shortname, bool basedat)
{
// Sanitize the basepath to be more predictable
basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar);
// First, organize by games so that we can do the right thing
BucketBy(SortedBy.Game, DedupeType.None, lower: false, norename: true);
// Create a temporary DAT to add things to
DatFile tempDat = new DatFile(this)
{
Name = null,
};
// Sort the input keys
List<string> keys = Keys;
keys.Sort(SplitByLevelSort);
// Then, we loop over the games
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 != Style.GetDirectoryName(key))
{
// Process and output the DAT
SplitByLevelHelper(tempDat, outDir, shortname, basedat);
// Reset the DAT for the next items
tempDat = new DatFile(this)
{
Name = null,
};
}
// Clean the input list and set all games to be pathless
List<DatItem> items = this[key];
items.ForEach(item => item.MachineName = Style.GetFileName(item.MachineName));
items.ForEach(item => item.MachineDescription = Style.GetFileName(item.MachineDescription));
// Now add the game to the output DAT
tempDat.AddRange(key, items);
// Then set the DAT name to be the parent directory name
tempDat.Name = Style.GetDirectoryName(key);
});
// Then we write the last DAT out since it would be skipped otherwise
SplitByLevelHelper(tempDat, outDir, shortname, basedat);
return true;
}
/// <summary>
/// Helper function for SplitByLevel to sort the input game names
/// </summary>
/// <param name="a">First string to compare</param>
/// <param name="b">Second string to compare</param>
/// <returns>-1 for a coming before b, 0 for a == b, 1 for a coming after b</returns>
private int SplitByLevelSort(string a, string b)
{
NaturalComparer nc = new NaturalComparer();
int adeep = a.Count(c => c == '/' || c == '\\');
int bdeep = b.Count(c => c == '/' || c == '\\');
if (adeep == bdeep)
{
return nc.Compare(a, b);
}
return adeep - bdeep;
}
/// <summary>
/// Helper function for SplitByLevel to clean and write out a DAT
/// </summary>
/// <param name="datFile">DAT to clean and write out</param>
/// <param name="outDir">Directory to write out to</param>
/// <param name="shortname">True if short naming scheme should be used, false otherwise</param>
/// <param name="restore">True if original filenames should be used as the base for output filename, false otherwise</param>
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 expName = name.Replace("/", " - ").Replace("\\", " - ");
// Get the path that the file will be written out to
string path = HttpUtility.HtmlDecode(String.IsNullOrEmpty(name)
? outDir
: Path.Combine(outDir, name));
// Now set the new output values
datFile.FileName = HttpUtility.HtmlDecode(String.IsNullOrEmpty(name)
? FileName
: (shortname
? Style.GetFileName(name)
: expName
)
);
datFile.FileName = (restore ? FileName + " (" + datFile.FileName + ")" : datFile.FileName);
datFile.Name = Name + " (" + expName + ")";
datFile.Description = (String.IsNullOrEmpty(Description) ? datFile.Name : Description + " (" + expName + ")");
datFile.Type = null;
// Write out the temporary DAT to the proper directory
datFile.WriteToFile(path);
}
/// <summary>
/// Split a DAT by type of Rom
/// </summary>
/// <param name="outDir">Name of the directory to write the DATs out to</param>
/// <param name="basepath">Parent path for replacement</param>
/// <returns>True if split succeeded, false otherwise</returns>
public bool SplitByType(string outDir, string basepath)
{
// Sanitize the basepath to be more predictable
basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar);
// 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,
};
// Now populate each of the DAT objects in turn
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> items = this[key];
foreach (DatItem item in items)
{
// If the file is a Rom
if (item.Type == ItemType.Rom)
{
romdat.Add(key, item);
}
// If the file is a Disk
else if (item.Type == ItemType.Disk)
{
diskdat.Add(key, item);
}
// If the file is a Sample
else if (item.Type == ItemType.Sample)
{
sampledat.Add(key, item);
}
}
});
// Get the output directory
if (outDir != "")
{
outDir = outDir + Path.GetDirectoryName(this.FileName).Remove(0, basepath.Length - 1);
}
else
{
outDir = Path.GetDirectoryName(this.FileName);
}
// Now, output all of the files to the output directory
Globals.Logger.User("DAT information created, outputting new files");
bool success = true;
success &= romdat.WriteToFile(outDir);
success &= diskdat.WriteToFile(outDir);
success &= sampledat.WriteToFile(outDir);
return success;
}
#endregion
#region Statistics
/// <summary>
/// Output the stats for the Dat in a human-readable format
/// </summary>
/// <param name="outputs">Dictionary representing the outputs</param>
/// <param name="statDatFormat">Set the statistics output format to use</param>
/// <param name="recalculate">True if numbers should be recalculated for the DAT, false otherwise (default)</param>
/// <param name="game">Number of games to use, -1 means recalculate games (default)</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise (default)</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise (default)</param>
public void OutputStats(Dictionary<StatDatFormat, StreamWriter> outputs, StatDatFormat statDatFormat,
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 + @"':
--------------------------------------------------
Uncompressed size: " + Style.GetBytesReadable(TotalSize) + @"
Games found: " + (game == -1 ? Keys.Count() : game) + @"
Roms found: " + RomCount + @"
Disks found: " + DiskCount + @"
Roms with CRC: " + CRCCount + @"
Roms with MD5: " + MD5Count + @"
Roms with SHA-1: " + SHA1Count + @"
Roms with SHA-256: " + SHA256Count + @"
Roms with SHA-384: " + SHA384Count + @"
Roms with SHA-512: " + SHA512Count + "\n";
if (baddumpCol)
{
results += " Roms with BadDump status: " + BaddumpCount + "\n";
}
if (nodumpCol)
{
results += " Roms with Nodump status: " + NodumpCount + "\n";
}
// For spacing between DATs
results += "\n\n";
Globals.Logger.User(results);
// Now write it out to file as well
string line = "";
if (outputs.ContainsKey(StatDatFormat.None))
{
line = @"'" + FileName + @"':
--------------------------------------------------
Uncompressed size: " + Style.GetBytesReadable(TotalSize) + @"
Games found: " + (game == -1 ? Keys.Count() : game) + @"
Roms found: " + RomCount + @"
Disks found: " + DiskCount + @"
Roms with CRC: " + CRCCount + @"
Roms with SHA-1: " + SHA1Count + @"
Roms with SHA-256: " + SHA256Count + @"
Roms with SHA-384: " + SHA384Count + @"
Roms with SHA-512: " + SHA512Count + "\n";
if (baddumpCol)
{
line += " Roms with BadDump status: " + BaddumpCount + "\n";
}
if (nodumpCol)
{
line += " Roms with Nodump status: " + NodumpCount + "\n";
}
// For spacing between DATs
line += "\n\n";
outputs[StatDatFormat.None].Write(line);
}
if (outputs.ContainsKey(StatDatFormat.CSV))
{
line = "\"" + FileName + "\","
+ "\"" + TotalSize + "\","
+ "\"" + (game == -1 ? Keys.Count() : game) + "\","
+ "\"" + RomCount + "\","
+ "\"" + DiskCount + "\","
+ "\"" + CRCCount + "\","
+ "\"" + MD5Count + "\","
+ "\"" + SHA1Count + "\","
+ "\"" + SHA256Count + "\","
+ "\"" + SHA384Count + "\","
+ "\"" + SHA512Count + "\"";
if (baddumpCol)
{
line += ",\"" + BaddumpCount + "\"";
}
if (nodumpCol)
{
line += ",\"" + NodumpCount + "\"";
}
line += "\n";
outputs[StatDatFormat.CSV].Write(line);
}
if (outputs.ContainsKey(StatDatFormat.HTML))
{
line = "\t\t\t<tr" + (FileName.StartsWith("DIR: ")
? " class=\"dir\"><td>" + HttpUtility.HtmlEncode(FileName.Remove(0, 5))
: "><td>" + HttpUtility.HtmlEncode(FileName)) + "</td>"
+ "<td align=\"right\">" + Style.GetBytesReadable(TotalSize) + "</td>"
+ "<td align=\"right\">" + (game == -1 ? Keys.Count() : game) + "</td>"
+ "<td align=\"right\">" + RomCount + "</td>"
+ "<td align=\"right\">" + DiskCount + "</td>"
+ "<td align=\"right\">" + CRCCount + "</td>"
+ "<td align=\"right\">" + MD5Count + "</td>"
+ "<td align=\"right\">" + SHA1Count + "</td>"
+ "<td align=\"right\">" + SHA256Count + "</td>";
if (baddumpCol)
{
line += "<td align=\"right\">" + BaddumpCount + "</td>";
}
if (nodumpCol)
{
line += "<td align=\"right\">" + NodumpCount + "</td>";
}
line += "</tr>\n";
outputs[StatDatFormat.HTML].Write(line);
}
if (outputs.ContainsKey(StatDatFormat.TSV))
{
line = "\"" + FileName + "\"\t"
+ "\"" + TotalSize + "\"\t"
+ "\"" + (game == -1 ? Keys.Count() : game) + "\"\t"
+ "\"" + RomCount + "\"\t"
+ "\"" + DiskCount + "\"\t"
+ "\"" + CRCCount + "\"\t"
+ "\"" + MD5Count + "\"\t"
+ "\"" + SHA1Count + "\"\t"
+ "\"" + SHA256Count + "\"\t"
+ "\"" + SHA384Count + "\"\t"
+ "\"" + SHA512Count + "\"";
if (baddumpCol)
{
line += "\t\"" + BaddumpCount + "\"";
}
if (nodumpCol)
{
line += "\t\"" + NodumpCount + "\"";
}
line += "\n";
outputs[StatDatFormat.TSV].Write(line);
}
}
/// <summary>
/// Recalculate the statistics for the Dat
/// </summary>
private void RecalculateStats()
{
// Wipe out any stats already there
_datStats.Reset();
// If we have a blank Dat in any way, return
if (this == null || Count == 0)
{
return;
}
// Loop through and add
List<string> keys = Keys;
Parallel.ForEach(keys, Globals.ParallelOptions, key =>
{
List<DatItem> items = this[key];
foreach (DatItem item in items)
{
_datStats.AddItem(item);
}
});
}
#endregion
#region Writing
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="datdata">All information for creating the datfile header</param>
/// <param name="outDir">Set the output directory</param>
/// <param name="norename">True if games should only be compared on game and file name (default), false if system and source are counted</param>
/// <param name="stats">True if DAT statistics should be output on write, false otherwise (default)</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="overwrite">True if files should be overwritten (default), false if they should be renamed instead</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public bool WriteToFile(string outDir, bool norename = true, bool stats = false, bool ignoreblanks = false, bool overwrite = true)
{
// If there's nothing there, abort
if (Count == 0)
{
Globals.Logger.User("There were no items to write out!");
return false;
}
// If output directory is empty, use the current folder
if (outDir == null || outDir.Trim() == "")
{
Globals.Logger.Verbose("No output directory defined, defaulting to curent folder");
outDir = Environment.CurrentDirectory;
}
// Create the output directory if it doesn't already exist
if (!Directory.Exists(outDir))
{
Directory.CreateDirectory(outDir);
}
// If the DAT has no output format, default to XML
if (DatFormat == 0)
{
Globals.Logger.Verbose("No DAT format defined, defaulting to XML");
DatFormat = DatFormat.Logiqx;
}
// Make sure that the three essential fields are filled in
if (String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description))
{
FileName = Name = Description = "Default";
}
else if (String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description))
{
FileName = Name = Description;
}
else if (String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description))
{
FileName = Description = Name;
}
else if (String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description))
{
FileName = Description;
}
else if (!String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description))
{
Name = Description = FileName;
}
else if (!String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description))
{
Name = Description;
}
else if (!String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description))
{
Description = Name;
}
else if (!String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description))
{
// Nothing is needed
}
// Output initial statistics, for kicks
if (stats)
{
OutputStats(new Dictionary<StatDatFormat, StreamWriter>(), StatDatFormat.None,
recalculate: (RomCount + DiskCount == 0), baddumpCol: true, nodumpCol: true);
}
// 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);
}
// Bucket roms by game name, if not already
BucketBy(SortedBy.Game, DedupeType.None, norename: norename);
// Output the number of items we're going to be writing
Globals.Logger.User("A total of {0} items will be written out to '{1}'", Count, FileName);
// Filter the DAT by 1G1R rules, if we're supposed to
// TODO: Create 1G1R logic before write
// If we are removing hashes, do that now
if (StripHash != 0x0)
{
StripHashesFromItems();
}
// If we are removing scene dates, do that now
if (SceneDateStrip)
{
StripSceneDatesFromItems();
}
// Get the outfile names
Dictionary<DatFormat, string> outfiles = Style.CreateOutfileNames(outDir, this, overwrite);
try
{
// Write out all required formats
Parallel.ForEach(outfiles.Keys, Globals.ParallelOptions, datFormat =>
{
string outfile = outfiles[datFormat];
try
{
switch (datFormat)
{
case DatFormat.AttractMode:
new AttractMode(this).WriteToFile(outfile);
break;
case DatFormat.ClrMamePro:
new ClrMamePro(this).WriteToFile(outfile, ignoreblanks);
break;
case DatFormat.CSV:
new SeparatedValue(this).WriteToFile(outfile, ',', ignoreblanks);
break;
case DatFormat.DOSCenter:
new DosCenter(this).WriteToFile(outfile, ignoreblanks);
break;
case DatFormat.Listroms:
new Listroms(this).WriteToFile(outfile, ignoreblanks);
break;
case DatFormat.Logiqx:
new Logiqx(this).WriteToFile(outfile, ignoreblanks);
break;
case DatFormat.MissFile:
new Missfile(this).WriteToFile(outfile, ignoreblanks);
break;
case DatFormat.OfflineList:
new OfflineList(this).WriteToFile(outfile, ignoreblanks);
break;
case DatFormat.RedumpMD5:
new Hashfile(this).WriteToFile(outfile, Hash.MD5, ignoreblanks);
break;
case DatFormat.RedumpSFV:
new Hashfile(this).WriteToFile(outfile, Hash.CRC, ignoreblanks);
break;
case DatFormat.RedumpSHA1:
new Hashfile(this).WriteToFile(outfile, Hash.SHA1, ignoreblanks);
break;
case DatFormat.RedumpSHA256:
new Hashfile(this).WriteToFile(outfile, Hash.SHA256, ignoreblanks);
break;
case DatFormat.RedumpSHA384:
new Hashfile(this).WriteToFile(outfile, Hash.SHA384, ignoreblanks);
break;
case DatFormat.RedumpSHA512:
new Hashfile(this).WriteToFile(outfile, Hash.SHA512, ignoreblanks);
break;
case DatFormat.RomCenter:
new RomCenter(this).WriteToFile(outfile, ignoreblanks);
break;
case DatFormat.SabreDat:
new SabreDat(this).WriteToFile(outfile, ignoreblanks);
break;
case DatFormat.SoftwareList:
new SoftwareList(this).WriteToFile(outfile, ignoreblanks);
break;
case DatFormat.TSV:
new SeparatedValue(this).WriteToFile(outfile, '\t', ignoreblanks);
break;
}
}
catch (Exception ex)
{
Globals.Logger.Error("Datfile {0} could not be written out: {1}", outfile, ex.ToString());
}
});
}
catch (Exception ex)
{
Globals.Logger.Error(ex.ToString());
return false;
}
return true;
}
#endregion
#endregion // Instance Methods
#region Static Methods
#region Statistics
/// <summary>
/// Output the stats for a list of input dats as files in a human-readable format
/// </summary>
/// <param name="inputs">List of input files and folders</param>
/// <param name="reportName">Name of the output file</param>
/// <param name="single">True if single DAT stats are output, false otherwise</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
/// <param name="statDatFormat" > Set the statistics output format to use</param>
public static void OutputStats(List<string> inputs, string reportName, string outDir, bool single,
bool baddumpCol, bool nodumpCol, StatDatFormat statDatFormat)
{
// If there's no output format, set the default
if (statDatFormat == 0x0)
{
statDatFormat = StatDatFormat.None;
}
// Get the proper output file name
if (String.IsNullOrEmpty(reportName))
{
reportName = "report";
}
outDir = Path.GetFullPath(outDir);
// Get the dictionary of desired output report names
Dictionary<StatDatFormat, string> outputs = Style.CreateOutStatsNames(outDir, statDatFormat, reportName);
// Make sure we have all files and then order them
List<string> files = FileTools.GetOnlyFilesFromInputs(inputs);
files = files
.OrderBy(i => Path.GetDirectoryName(i))
.ThenBy(i => Path.GetFileName(i))
.ToList();
// Create output writers based on filenames
Dictionary<StatDatFormat, StreamWriter> writers = new Dictionary<StatDatFormat, StreamWriter>();
foreach (KeyValuePair<StatDatFormat, string> kvp in outputs)
{
FileStream fs = FileTools.TryCreate(kvp.Value);
if (fs != null)
{
writers.Add(kvp.Key, new StreamWriter(fs));
}
}
// Write the header, if any
WriteStatsHeader(writers, statDatFormat, baddumpCol, nodumpCol);
// 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
WriteStatsMidSeparator(writers, statDatFormat, baddumpCol, nodumpCol);
DatFile lastdirdat = new DatFile
{
FileName = "DIR: " + HttpUtility.HtmlEncode(lastdir.Remove(0, basepath.Length + (basepath.Length == 0 ? 0 : 1))),
_datStats = dirStats,
};
lastdirdat.OutputStats(writers, statDatFormat, game: dirStats.GameCount, baddumpCol: baddumpCol, nodumpCol: nodumpCol);
// Write the mid-footer, if any
WriteStatsFooterSeparator(writers, statDatFormat, baddumpCol, nodumpCol);
// Write the header, if any
WriteStatsMidHeader(writers, statDatFormat, baddumpCol, nodumpCol);
// Reset the directory stats
dirStats.Reset();
}
Globals.Logger.Verbose("Beginning stat collection for '{0}'", false, file);
List<string> games = new List<string>();
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 '{0}'\n", false, file);
if (single)
{
datdata.OutputStats(writers, statDatFormat,
baddumpCol: baddumpCol, nodumpCol: nodumpCol);
}
// 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
WriteStatsMidSeparator(writers, statDatFormat, baddumpCol, nodumpCol);
if (single)
{
DatFile dirdat = new DatFile
{
FileName = "DIR: " + HttpUtility.HtmlEncode(lastdir.Remove(0, basepath.Length + (basepath.Length == 0 ? 0 : 1))),
_datStats = dirStats,
};
dirdat.OutputStats(writers, statDatFormat, game: dirStats.GameCount, baddumpCol: baddumpCol, nodumpCol: nodumpCol);
}
// Write the mid-footer, if any
WriteStatsFooterSeparator(writers, statDatFormat, baddumpCol, nodumpCol);
// Write the header, if any
WriteStatsMidHeader(writers, statDatFormat, baddumpCol, nodumpCol);
// Reset the directory stats
dirStats.Reset();
// Output total DAT stats
DatFile totaldata = new DatFile
{
FileName = "DIR: All DATs",
_datStats = totalStats,
};
totaldata.OutputStats(writers, statDatFormat, game: totalStats.GameCount, baddumpCol: baddumpCol, nodumpCol: nodumpCol);
// Output footer if needed
WriteStatsFooter(writers, statDatFormat);
// Flush and dispose of the stream writers
foreach (StatDatFormat format in outputs.Keys)
{
writers[format].Flush();
writers[format].Dispose();
}
Globals.Logger.User(@"
Please check the log folder if the stats scrolled offscreen", false);
}
/// <summary>
/// Write out the header to the stream, if any exists
/// </summary>
/// <param name="outputs">Dictionary representing the outputs</param>
/// <param name="statDatFormat">StatDatFormat representing output format</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
private static void WriteStatsHeader(Dictionary<StatDatFormat, StreamWriter> outputs, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol)
{
if (outputs.ContainsKey(StatDatFormat.None))
{
// Nothing
}
if (outputs.ContainsKey(StatDatFormat.CSV))
{
outputs[StatDatFormat.CSV].Write("\"File Name\",\"Total Size\",\"Games\",\"Roms\",\"Disks\",\"# with CRC\",\"# with MD5\",\"# with SHA-1\",\"# with SHA-256\""
+ (baddumpCol ? ",\"BadDumps\"" : "") + (nodumpCol ? ",\"Nodumps\"" : "") + "\n");
}
if (outputs.ContainsKey(StatDatFormat.HTML))
{
outputs[StatDatFormat.HTML].Write(@"<!DOCTYPE html>
<html>
<header>
<title>DAT Statistics Report</title>
<style>
body {
background-color: lightgray;
}
.dir {
color: #0088FF;
}
.right {
align: right;
}
</style>
</header>
<body>
<h2>DAT Statistics Report (" + DateTime.Now.ToShortDateString() + @")</h2>
<table border=""1"" cellpadding=""5"" cellspacing=""0"">
");
}
if (outputs.ContainsKey(StatDatFormat.TSV))
{
outputs[StatDatFormat.TSV].Write("\"File Name\"\t\"Total Size\"\t\"Games\"\t\"Roms\"\t\"Disks\"\t\"# with CRC\"\t\"# with MD5\"\t\"# with SHA-1\"\t\"# with SHA-256\""
+ (baddumpCol ? "\t\"BadDumps\"" : "") + (nodumpCol ? "\t\"Nodumps\"" : "") + "\n");
}
// Now write the mid header for those who need it
WriteStatsMidHeader(outputs, statDatFormat, baddumpCol, nodumpCol);
}
/// <summary>
/// Write out the mid-header to the stream, if any exists
/// </summary>
/// <param name="outputs">Dictionary representing the outputs</param>
/// <param name="statDatFormat">StatDatFormat representing output format</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
private static void WriteStatsMidHeader(Dictionary<StatDatFormat, StreamWriter> outputs, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol)
{
if (outputs.ContainsKey(StatDatFormat.None))
{
// Nothing
}
if (outputs.ContainsKey(StatDatFormat.CSV))
{
// Nothing
}
if (outputs.ContainsKey(StatDatFormat.HTML))
{
outputs[StatDatFormat.HTML].Write(@" <tr bgcolor=""gray""><th>File Name</th><th align=""right"">Total Size</th><th align=""right"">Games</th><th align=""right"">Roms</th>"
+ @"<th align=""right"">Disks</th><th align=""right"">&#35; with CRC</th><th align=""right"">&#35; with MD5</th><th align=""right"">&#35; with SHA-1</th><th align=""right"">&#35; with SHA-256</th>"
+ (baddumpCol ? "<th class=\".right\">Baddumps</th>" : "") + (nodumpCol ? "<th class=\".right\">Nodumps</th>" : "") + "</tr>\n");
}
if (outputs.ContainsKey(StatDatFormat.TSV))
{
// Nothing
}
}
/// <summary>
/// Write out the separator to the stream, if any exists
/// </summary>
/// <param name="outputs">Dictionary representing the outputs</param>
/// <param name="statDatFormat">StatDatFormat representing output format</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
private static void WriteStatsMidSeparator(Dictionary<StatDatFormat, StreamWriter> outputs, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol)
{
if (outputs.ContainsKey(StatDatFormat.None))
{
// Nothing
}
if (outputs.ContainsKey(StatDatFormat.CSV))
{
// Nothing
}
if (outputs.ContainsKey(StatDatFormat.HTML))
{
outputs[StatDatFormat.HTML].Write("<tr><td colspan=\""
+ (baddumpCol && nodumpCol
? "12"
: (baddumpCol ^ nodumpCol
? "11"
: "10")
)
+ "\"></td></tr>\n");
}
if (outputs.ContainsKey(StatDatFormat.TSV))
{
// Nothing
}
}
/// <summary>
/// Write out the footer-separator to the stream, if any exists
/// </summary>
/// <param name="outputs">Dictionary representing the outputs</param>
/// <param name="statDatFormat">StatDatFormat representing output format</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
private static void WriteStatsFooterSeparator(Dictionary<StatDatFormat, StreamWriter> outputs, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol)
{
if (outputs.ContainsKey(StatDatFormat.None))
{
outputs[StatDatFormat.None].Write("\n");
}
if (outputs.ContainsKey(StatDatFormat.CSV))
{
outputs[StatDatFormat.CSV].Write("\n");
}
if (outputs.ContainsKey(StatDatFormat.HTML))
{
outputs[StatDatFormat.HTML].Write("<tr border=\"0\"><td colspan=\""
+ (baddumpCol && nodumpCol
? "12"
: (baddumpCol ^ nodumpCol
? "11"
: "10")
)
+ "\"></td></tr>\n");
}
if (outputs.ContainsKey(StatDatFormat.TSV))
{
outputs[StatDatFormat.TSV].Write("\n");
}
}
/// <summary>
/// Write out the footer to the stream, if any exists
/// </summary>
/// <param name="sw">StreamWriter representing the output</param>
/// <param name="statDatFormat">StatDatFormat representing output format</param>
private static void WriteStatsFooter(Dictionary<StatDatFormat, StreamWriter> outputs, StatDatFormat statDatFormat)
{
if (outputs.ContainsKey(StatDatFormat.None))
{
// Nothing
}
if (outputs.ContainsKey(StatDatFormat.CSV))
{
// Nothing
}
if (outputs.ContainsKey(StatDatFormat.HTML))
{
outputs[StatDatFormat.HTML].Write(@" </table>
</body>
</html>
");
}
if (outputs.ContainsKey(StatDatFormat.TSV))
{
// Nothing
}
}
#endregion
#endregion // Static Methods
}
}