Compare commits

..

14 Commits
1.2.0 ... 1.3.2

Author SHA1 Message Date
Matt Nadareski
3f9b09b943 Bump version 2023-12-05 11:04:02 -05:00
Matt Nadareski
2b1ee393d4 Fix some formatting issues 2023-12-04 14:33:56 -05:00
Matt Nadareski
887c443a17 Correct log statement for universal hash 2023-12-04 14:15:36 -05:00
Matt Nadareski
5ad9bebf88 Bump version 2023-11-22 23:13:09 -05:00
Matt Nadareski
8c8e624ac2 Update compatiblity libraries 2023-11-22 23:12:44 -05:00
Matt Nadareski
d7b2d13d8b Bump version 2023-11-22 10:12:09 -05:00
Matt Nadareski
8c8ae49a3b Support .NET Framework 2.0 2023-11-22 10:11:56 -05:00
Matt Nadareski
b77eec5063 Use different set of libraries 2023-11-22 09:44:04 -05:00
Matt Nadareski
5d992566b5 Update SabreTools.Models 2023-11-22 09:42:15 -05:00
Matt Nadareski
46bde960f3 Perform some prep for .NET Framework 3.5 2023-11-20 21:45:27 -05:00
Matt Nadareski
ba2c3a592f Support async in .NET Framework 4.0 2023-11-20 21:18:36 -05:00
Matt Nadareski
f0bca60d63 Bump version 2023-11-16 12:21:19 -05:00
Matt Nadareski
a81dc6d680 Rename Writer to Formatter 2023-11-16 09:36:00 -05:00
Matt Nadareski
1a6ebfdbf0 Add more logic from MPF, update syntax 2023-11-16 00:59:40 -05:00
12 changed files with 2561 additions and 268 deletions

View File

@@ -23,7 +23,7 @@ namespace SabreTools.RedumpLib.Attributes
// If the value returns a null on ToString, just return null
string? valueStr = value?.ToString();
if (string.IsNullOrWhiteSpace(valueStr))
if (string.IsNullOrEmpty(valueStr))
return null;
// Get the member info array

765
Builder.cs Normal file
View File

@@ -0,0 +1,765 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using Newtonsoft.Json;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.Web;
namespace SabreTools.RedumpLib
{
public static class Builder
{
#region Creation
/// <summary>
/// Create a SubmissionInfo from a JSON file path
/// </summary>
/// <param name="path">Path to the SubmissionInfo JSON</param>
/// <returns>Filled SubmissionInfo on success, null on error</returns>
public static SubmissionInfo? CreateFromFile(string? path)
{
// If the path is invalid
if (string.IsNullOrEmpty(path))
return null;
// If the file doesn't exist
if (!File.Exists(path))
return null;
// Try to open and deserialize the file
try
{
byte[] data = File.ReadAllBytes(path);
string dataString = Encoding.UTF8.GetString(data);
return JsonConvert.DeserializeObject<SubmissionInfo>(dataString);
}
catch
{
// We don't care what the exception was
return null;
}
}
/// <summary>
/// Create a new SubmissionInfo object from a disc page
/// </summary>
/// <param name="discData">String containing the HTML disc data</param>
/// <returns>Filled SubmissionInfo object on success, null on error</returns>
/// <remarks>Not currently working</remarks>
private static SubmissionInfo? CreateFromID(string discData)
{
var info = new SubmissionInfo()
{
CommonDiscInfo = new CommonDiscInfoSection(),
VersionAndEditions = new VersionAndEditionsSection(),
};
// No disc data means we can't parse it
if (string.IsNullOrEmpty(discData))
return null;
try
{
// Load the current disc page into an XML document
var redumpPage = new XmlDocument() { PreserveWhitespace = true };
redumpPage.LoadXml(discData);
// If the current page isn't valid, we can't parse it
if (!redumpPage.HasChildNodes)
return null;
// Get the body node, if possible
var bodyNode = redumpPage["html"]?["body"];
if (bodyNode == null || !bodyNode.HasChildNodes)
return null;
// Loop through and get the main node, if possible
XmlNode? mainNode = null;
foreach (XmlNode? tempNode in bodyNode.ChildNodes)
{
// Invalid nodes are skipped
if (tempNode == null)
continue;
// We only care about div elements
if (!string.Equals(tempNode.Name, "div", StringComparison.OrdinalIgnoreCase))
continue;
// We only care if it has attributes
if (tempNode.Attributes == null)
continue;
// The main node has a class of "main"
if (string.Equals(tempNode.Attributes["class"]?.Value, "main", StringComparison.OrdinalIgnoreCase))
{
mainNode = tempNode;
break;
}
}
// If the main node is invalid, we can't do anything
if (mainNode == null || !mainNode.HasChildNodes)
return null;
// Try to find elements as we're going
foreach (XmlNode? childNode in mainNode.ChildNodes)
{
// Invalid nodes are skipped
if (childNode == null)
continue;
// The title is the only thing in h1 tags
if (string.Equals(childNode.Name, "h1", StringComparison.OrdinalIgnoreCase))
info.CommonDiscInfo.Title = childNode.InnerText;
// Most things are div elements but can be hard to parse out
else if (!string.Equals(childNode.Name, "div", StringComparison.OrdinalIgnoreCase))
continue;
// Only 2 of the internal divs have classes attached and one is not used here
if (childNode.Attributes != null && string.Equals(childNode.Attributes["class"]?.Value, "game",
StringComparison.OrdinalIgnoreCase))
{
// If we don't have children nodes, skip this one over
if (!childNode.HasChildNodes)
continue;
// The game node contains multiple other elements
foreach (XmlNode? gameNode in childNode.ChildNodes)
{
// Invalid nodes are skipped
if (gameNode == null)
continue;
// Table elements contain multiple other parts of information
if (string.Equals(gameNode.Name, "table", StringComparison.OrdinalIgnoreCase))
{
// All tables have some attribute we can use
if (gameNode.Attributes == null)
continue;
// The gameinfo node contains most of the major information
if (string.Equals(gameNode.Attributes["class"]?.Value, "gameinfo",
StringComparison.OrdinalIgnoreCase))
{
// If we don't have children nodes, skip this one over
if (!gameNode.HasChildNodes)
continue;
// Loop through each of the rows
foreach (XmlNode? gameInfoNode in gameNode.ChildNodes)
{
// Invalid nodes are skipped
if (gameInfoNode == null)
continue;
// If we run into anything not a row, ignore it
if (!string.Equals(gameInfoNode.Name, "tr", StringComparison.OrdinalIgnoreCase))
continue;
// If we don't have the required nodes, ignore it
if (gameInfoNode["th"] == null || gameInfoNode["td"] == null)
continue;
var gameInfoNodeHeader = gameInfoNode["th"];
var gameInfoNodeData = gameInfoNode["td"];
if (gameInfoNodeHeader == null || gameInfoNodeData == null)
{
// No-op for invalid data
}
else if (string.Equals(gameInfoNodeHeader.InnerText, "System", StringComparison.OrdinalIgnoreCase))
{
info.CommonDiscInfo.System = Extensions.ToRedumpSystem(gameInfoNodeData["a"]?.InnerText ?? string.Empty);
}
else if (string.Equals(gameInfoNodeHeader.InnerText, "Media", StringComparison.OrdinalIgnoreCase))
{
info.CommonDiscInfo.Media = Extensions.ToDiscType(gameInfoNodeData.InnerText);
}
else if (string.Equals(gameInfoNodeHeader.InnerText, "Category", StringComparison.OrdinalIgnoreCase))
{
info.CommonDiscInfo.Category = Extensions.ToDiscCategory(gameInfoNodeData.InnerText);
}
else if (string.Equals(gameInfoNodeHeader.InnerText, "Region", StringComparison.OrdinalIgnoreCase))
{
// TODO: COMPLETE
}
else if (string.Equals(gameInfoNodeHeader.InnerText, "Languages", StringComparison.OrdinalIgnoreCase))
{
// TODO: COMPLETE
}
else if (string.Equals(gameInfoNodeHeader.InnerText, "Edition", StringComparison.OrdinalIgnoreCase))
{
info.VersionAndEditions.OtherEditions = gameInfoNodeData.InnerText;
}
else if (string.Equals(gameInfoNodeHeader.InnerText, "Added", StringComparison.OrdinalIgnoreCase))
{
if (DateTime.TryParse(gameInfoNodeData.InnerText, out DateTime added))
info.Added = added;
}
else if (string.Equals(gameInfoNodeHeader.InnerText, "Last modified", StringComparison.OrdinalIgnoreCase))
{
if (DateTime.TryParse(gameInfoNodeData.InnerText, out DateTime lastModified))
info.LastModified = lastModified;
}
}
}
// The gamecomments node contains way more than it implies
if (string.Equals(gameNode.Attributes["class"]?.Value, "gamecomments", StringComparison.OrdinalIgnoreCase))
{
// TODO: COMPLETE
}
// TODO: COMPLETE
}
// The only other supported elements are divs
else if (!string.Equals(gameNode.Name, "div", StringComparison.OrdinalIgnoreCase))
{
continue;
}
// Check the div for dumper info
// TODO: COMPLETE
}
}
// Figure out what the div contains, if possible
// TODO: COMPLETE
}
}
catch
{
return null;
}
return info;
}
/// <summary>
/// Fill out an existing SubmissionInfo object based on a disc page
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <param name="id">Redump disc ID to retrieve</param>
/// <param name="includeAllData">True to include all pullable information, false to do bare minimum</param>
#if NETFRAMEWORK
public async static Task<bool> FillFromId(RedumpWebClient wc, SubmissionInfo info, int id, bool includeAllData)
#else
public async static Task<bool> FillFromId(RedumpHttpClient wc, SubmissionInfo info, int id, bool includeAllData)
#endif
{
// Ensure that required sections exist
info = EnsureAllSections(info);
#if NET40
var discData = await Task.Factory.StartNew(() => wc.DownloadSingleSiteID(id));
#elif NETFRAMEWORK
var discData = await Task.Run(() => wc.DownloadSingleSiteID(id));
#else
var discData = await wc.DownloadSingleSiteID(id);
#endif
if (string.IsNullOrEmpty(discData))
return false;
// Title, Disc Number/Letter, Disc Title
var match = Constants.TitleRegex.Match(discData);
if (match.Success)
{
string? title = WebUtility.HtmlDecode(match.Groups[1].Value);
// If we have parenthesis, title is everything before the first one
int firstParenLocation = title?.IndexOf(" (") ?? -1;
if (title != null && firstParenLocation >= 0)
{
info.CommonDiscInfo!.Title = title.Substring(0, firstParenLocation);
var subMatches = Constants.DiscNumberLetterRegex.Matches(title);
foreach (Match subMatch in subMatches.Cast<Match>())
{
var subMatchValue = subMatch.Groups[1].Value;
// Disc number or letter
if (subMatchValue.StartsWith("Disc"))
info.CommonDiscInfo.DiscNumberLetter = subMatchValue.Remove(0, "Disc ".Length);
// Issue number
else if (subMatchValue.All(c => char.IsNumber(c)))
info.CommonDiscInfo.Title += $" ({subMatchValue})";
// Disc title
else
info.CommonDiscInfo.DiscTitle = subMatchValue;
}
}
// Otherwise, leave the title as-is
else
{
info.CommonDiscInfo!.Title = title;
}
}
// Foreign Title
match = Constants.ForeignTitleRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo!.ForeignTitleNonLatin = WebUtility.HtmlDecode(match.Groups[1].Value);
else
info.CommonDiscInfo!.ForeignTitleNonLatin = null;
// Category
match = Constants.CategoryRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.Category = Extensions.ToDiscCategory(match.Groups[1].Value);
else
info.CommonDiscInfo.Category = DiscCategory.Games;
// Region
if (info.CommonDiscInfo.Region == null)
{
match = Constants.RegionRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.Region = Extensions.ToRegion(match.Groups[1].Value);
}
// Languages
var matches = Constants.LanguagesRegex.Matches(discData);
if (matches.Count > 0)
{
var tempLanguages = new List<Language?>();
foreach (Match submatch in matches.Cast<Match>())
{
tempLanguages.Add(Extensions.ToLanguage(submatch.Groups[1].Value));
}
info.CommonDiscInfo.Languages = tempLanguages.Where(l => l != null).ToArray();
}
// Serial
if (includeAllData)
{
// TODO: Re-enable if there's a way of verifying against a disc
//match = Constants.SerialRegex.Match(discData);
//if (match.Success)
// info.CommonDiscInfo.Serial = $"(VERIFY THIS) {WebUtility.HtmlDecode(match.Groups[1].Value)}";
}
// Error count
if (string.IsNullOrEmpty(info.CommonDiscInfo.ErrorsCount))
{
match = Constants.ErrorCountRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.ErrorsCount = match.Groups[1].Value;
}
// Version
if (info.VersionAndEditions!.Version == null)
{
match = Constants.VersionRegex.Match(discData);
if (match.Success)
info.VersionAndEditions.Version = $"(VERIFY THIS) {WebUtility.HtmlDecode(match.Groups[1].Value)}";
}
// Dumpers
matches = Constants.DumpersRegex.Matches(discData);
if (matches.Count > 0)
{
// Start with any currently listed dumpers
var tempDumpers = new List<string>();
if (info.DumpersAndStatus!.Dumpers != null && info.DumpersAndStatus.Dumpers.Length > 0)
{
foreach (string dumper in info.DumpersAndStatus.Dumpers)
tempDumpers.Add(dumper);
}
foreach (Match submatch in matches.Cast<Match>())
{
string? dumper = WebUtility.HtmlDecode(submatch.Groups[1].Value);
if (dumper != null)
tempDumpers.Add(dumper);
}
info.DumpersAndStatus.Dumpers = [.. tempDumpers];
}
// TODO: Unify handling of fields that can include site codes (Comments/Contents)
// Comments
if (includeAllData)
{
match = Constants.CommentsRegex.Match(discData);
if (match.Success)
{
// Process the old comments block
string oldComments = info.CommonDiscInfo.Comments
+ (string.IsNullOrEmpty(info.CommonDiscInfo.Comments) ? string.Empty : "\n")
+ (WebUtility.HtmlDecode(match.Groups[1].Value) ?? string.Empty)
.Replace("\r\n", "\n")
.Replace("<br />\n", "\n")
.Replace("<br />", string.Empty)
.Replace("</div>", string.Empty)
.Replace("[+]", string.Empty)
.ReplaceHtmlWithSiteCodes();
oldComments = Regex.Replace(oldComments, @"<div .*?>", string.Empty, RegexOptions.Compiled);
// Create state variables
bool addToLast = false;
SiteCode? lastSiteCode = null;
string newComments = string.Empty;
// Process the comments block line-by-line
string[] commentsSeparated = oldComments.Split('\n');
for (int i = 0; i < commentsSeparated.Length; i++)
{
string commentLine = commentsSeparated[i].Trim();
// If we have an empty line, we want to treat this as intentional
if (string.IsNullOrEmpty(commentLine))
{
addToLast = false;
lastSiteCode = null;
newComments += $"{commentLine}\n";
continue;
}
// Otherwise, we need to find what tag is in use
bool foundTag = false;
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
// If we have a null site code, just skip
if (siteCode == null)
continue;
// If the line doesn't contain this tag, just skip
var shortName = siteCode.ShortName();
if (shortName == null || !commentLine.Contains(shortName))
continue;
// Mark as having found a tag
foundTag = true;
// Cache the current site code
lastSiteCode = siteCode;
// A subset of tags can be multiline
addToLast = siteCode.IsMultiLine();
// Skip certain site codes because of data issues
switch (siteCode)
{
// Multiple
case SiteCode.InternalSerialName:
case SiteCode.Multisession:
case SiteCode.VolumeLabel:
continue;
// Audio CD
case SiteCode.RingNonZeroDataStart:
case SiteCode.UniversalHash:
continue;
// Microsoft Xbox and Xbox 360
case SiteCode.DMIHash:
case SiteCode.PFIHash:
case SiteCode.SSHash:
case SiteCode.SSVersion:
case SiteCode.XMID:
case SiteCode.XeMID:
continue;
// Microsoft Xbox One and Series X/S
case SiteCode.Filename:
continue;
// Nintendo Gamecube
case SiteCode.InternalName:
continue;
}
// If we don't already have this site code, add it to the dictionary
if (!info.CommonDiscInfo.CommentsSpecialFields!.ContainsKey(siteCode.Value))
info.CommonDiscInfo.CommentsSpecialFields[siteCode.Value] = $"(VERIFY THIS) {commentLine.Replace(shortName, string.Empty).Trim()}";
// Otherwise, append the value to the existing key
else
info.CommonDiscInfo.CommentsSpecialFields[siteCode.Value] += $", {commentLine.Replace(shortName, string.Empty).Trim()}";
break;
}
// If we didn't find a known tag, just add the line, just in case
if (!foundTag)
{
if (addToLast && lastSiteCode != null)
{
if (!string.IsNullOrEmpty(info.CommonDiscInfo.CommentsSpecialFields![lastSiteCode.Value]))
info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode.Value] += "\n";
info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode.Value] += commentLine;
}
else
{
newComments += $"{commentLine}\n";
}
}
}
// Set the new comments field
info.CommonDiscInfo.Comments = newComments;
}
}
// Contents
if (includeAllData)
{
match = Constants.ContentsRegex.Match(discData);
if (match.Success)
{
// Process the old contents block
string oldContents = info.CommonDiscInfo.Contents
+ (string.IsNullOrEmpty(info.CommonDiscInfo.Contents) ? string.Empty : "\n")
+ (WebUtility.HtmlDecode(match.Groups[1].Value) ?? string.Empty)
.Replace("\r\n", "\n")
.Replace("<br />\n", "\n")
.Replace("<br />", string.Empty)
.Replace("</div>", string.Empty)
.Replace("[+]", string.Empty)
.ReplaceHtmlWithSiteCodes();
oldContents = Regex.Replace(oldContents, @"<div .*?>", string.Empty, RegexOptions.Compiled);
// Create state variables
bool addToLast = false;
SiteCode? lastSiteCode = null;
string newContents = string.Empty;
// Process the contents block line-by-line
string[] contentsSeparated = oldContents.Split('\n');
for (int i = 0; i < contentsSeparated.Length; i++)
{
string contentLine = contentsSeparated[i].Trim();
// If we have an empty line, we want to treat this as intentional
if (string.IsNullOrEmpty(contentLine))
{
addToLast = false;
lastSiteCode = null;
newContents += $"{contentLine}\n";
continue;
}
// Otherwise, we need to find what tag is in use
bool foundTag = false;
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
// If we have a null site code, just skip
if (siteCode == null)
continue;
// If the line doesn't contain this tag, just skip
var shortName = siteCode.ShortName();
if (shortName == null || !contentLine.Contains(shortName))
continue;
// Cache the current site code
lastSiteCode = siteCode;
// If we don't already have this site code, add it to the dictionary
if (!info.CommonDiscInfo.ContentsSpecialFields!.ContainsKey(siteCode.Value))
info.CommonDiscInfo.ContentsSpecialFields[siteCode.Value] = $"(VERIFY THIS) {contentLine.Replace(shortName, string.Empty).Trim()}";
// A subset of tags can be multiline
addToLast = siteCode.IsMultiLine();
// Mark as having found a tag
foundTag = true;
break;
}
// If we didn't find a known tag, just add the line, just in case
if (!foundTag)
{
if (addToLast && lastSiteCode != null)
{
if (!string.IsNullOrEmpty(info.CommonDiscInfo.ContentsSpecialFields![lastSiteCode.Value]))
info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode.Value] += "\n";
info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode.Value] += contentLine;
}
else
{
newContents += $"{contentLine}\n";
}
}
}
// Set the new contents field
info.CommonDiscInfo.Contents = newContents;
}
}
// Added
match = Constants.AddedRegex.Match(discData);
if (match.Success)
{
if (DateTime.TryParse(match.Groups[1].Value, out DateTime added))
info.Added = added;
else
info.Added = null;
}
// Last Modified
match = Constants.LastModifiedRegex.Match(discData);
if (match.Success)
{
if (DateTime.TryParse(match.Groups[1].Value, out DateTime lastModified))
info.LastModified = lastModified;
else
info.LastModified = null;
}
return true;
}
/// <summary>
/// Ensure all required sections in a submission info exist
/// </summary>
/// <param name="info">SubmissionInfo object to verify</param>
public static SubmissionInfo EnsureAllSections(SubmissionInfo? info)
{
// If there's no info, create one
info ??= new SubmissionInfo();
// Ensure all sections
info.CommonDiscInfo ??= new CommonDiscInfoSection();
info.VersionAndEditions ??= new VersionAndEditionsSection();
info.EDC ??= new EDCSection();
info.ParentCloneRelationship ??= new ParentCloneRelationshipSection();
info.Extras ??= new ExtrasSection();
info.CopyProtection ??= new CopyProtectionSection();
info.DumpersAndStatus ??= new DumpersAndStatusSection();
info.TracksAndWriteOffsets ??= new TracksAndWriteOffsetsSection();
info.SizeAndChecksums ??= new SizeAndChecksumsSection();
info.DumpingInfo ??= new DumpingInfoSection();
// Ensure special dictionaries
info.CommonDiscInfo.CommentsSpecialFields ??= [];
info.CommonDiscInfo.ContentsSpecialFields ??= [];
return info;
}
/// <summary>
/// Inject information from a seed SubmissionInfo into the existing one
/// </summary>
/// <param name="info">Existing submission information</param>
/// <param name="seed">User-supplied submission information</param>
public static void InjectSubmissionInformation(SubmissionInfo? info, SubmissionInfo? seed)
{
// If we have any invalid info
if (seed == null)
return;
// Ensure that required sections exist
info = EnsureAllSections(info);
// Otherwise, inject information as necessary
if (info.CommonDiscInfo != null && seed.CommonDiscInfo != null)
{
// Info that only overwrites if supplied
if (!string.IsNullOrEmpty(seed.CommonDiscInfo.Title)) info.CommonDiscInfo.Title = seed.CommonDiscInfo.Title;
if (!string.IsNullOrEmpty(seed.CommonDiscInfo.ForeignTitleNonLatin)) info.CommonDiscInfo.ForeignTitleNonLatin = seed.CommonDiscInfo.ForeignTitleNonLatin;
if (!string.IsNullOrEmpty(seed.CommonDiscInfo.DiscNumberLetter)) info.CommonDiscInfo.DiscNumberLetter = seed.CommonDiscInfo.DiscNumberLetter;
if (!string.IsNullOrEmpty(seed.CommonDiscInfo.DiscTitle)) info.CommonDiscInfo.DiscTitle = seed.CommonDiscInfo.DiscTitle;
if (seed.CommonDiscInfo.Category != null) info.CommonDiscInfo.Category = seed.CommonDiscInfo.Category;
if (seed.CommonDiscInfo.Region != null) info.CommonDiscInfo.Region = seed.CommonDiscInfo.Region;
if (seed.CommonDiscInfo.Languages != null) info.CommonDiscInfo.Languages = seed.CommonDiscInfo.Languages;
if (seed.CommonDiscInfo.LanguageSelection != null) info.CommonDiscInfo.LanguageSelection = seed.CommonDiscInfo.LanguageSelection;
if (!string.IsNullOrEmpty(seed.CommonDiscInfo.Serial)) info.CommonDiscInfo.Serial = seed.CommonDiscInfo.Serial;
if (!string.IsNullOrEmpty(seed.CommonDiscInfo.Barcode)) info.CommonDiscInfo.Barcode = seed.CommonDiscInfo.Barcode;
if (!string.IsNullOrEmpty(seed.CommonDiscInfo.Comments)) info.CommonDiscInfo.Comments = seed.CommonDiscInfo.Comments;
if (seed.CommonDiscInfo.CommentsSpecialFields != null) info.CommonDiscInfo.CommentsSpecialFields = seed.CommonDiscInfo.CommentsSpecialFields;
if (!string.IsNullOrEmpty(seed.CommonDiscInfo.Contents)) info.CommonDiscInfo.Contents = seed.CommonDiscInfo.Contents;
if (seed.CommonDiscInfo.ContentsSpecialFields != null) info.CommonDiscInfo.ContentsSpecialFields = seed.CommonDiscInfo.ContentsSpecialFields;
// Info that always overwrites
info.CommonDiscInfo.Layer0MasteringRing = seed.CommonDiscInfo.Layer0MasteringRing;
info.CommonDiscInfo.Layer0MasteringSID = seed.CommonDiscInfo.Layer0MasteringSID;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = seed.CommonDiscInfo.Layer0ToolstampMasteringCode;
info.CommonDiscInfo.Layer0MouldSID = seed.CommonDiscInfo.Layer0MouldSID;
info.CommonDiscInfo.Layer0AdditionalMould = seed.CommonDiscInfo.Layer0AdditionalMould;
info.CommonDiscInfo.Layer1MasteringRing = seed.CommonDiscInfo.Layer1MasteringRing;
info.CommonDiscInfo.Layer1MasteringSID = seed.CommonDiscInfo.Layer1MasteringSID;
info.CommonDiscInfo.Layer1ToolstampMasteringCode = seed.CommonDiscInfo.Layer1ToolstampMasteringCode;
info.CommonDiscInfo.Layer1MouldSID = seed.CommonDiscInfo.Layer1MouldSID;
info.CommonDiscInfo.Layer1AdditionalMould = seed.CommonDiscInfo.Layer1AdditionalMould;
info.CommonDiscInfo.Layer2MasteringRing = seed.CommonDiscInfo.Layer2MasteringRing;
info.CommonDiscInfo.Layer2MasteringSID = seed.CommonDiscInfo.Layer2MasteringSID;
info.CommonDiscInfo.Layer2ToolstampMasteringCode = seed.CommonDiscInfo.Layer2ToolstampMasteringCode;
info.CommonDiscInfo.Layer3MasteringRing = seed.CommonDiscInfo.Layer3MasteringRing;
info.CommonDiscInfo.Layer3MasteringSID = seed.CommonDiscInfo.Layer3MasteringSID;
info.CommonDiscInfo.Layer3ToolstampMasteringCode = seed.CommonDiscInfo.Layer3ToolstampMasteringCode;
}
if (info.VersionAndEditions != null && seed.VersionAndEditions != null)
{
// Info that only overwrites if supplied
if (!string.IsNullOrEmpty(seed.VersionAndEditions.Version)) info.VersionAndEditions.Version = seed.VersionAndEditions.Version;
if (!string.IsNullOrEmpty(seed.VersionAndEditions.OtherEditions)) info.VersionAndEditions.OtherEditions = seed.VersionAndEditions.OtherEditions;
}
if (info.CopyProtection != null && seed.CopyProtection != null)
{
// Info that only overwrites if supplied
if (!string.IsNullOrEmpty(seed.CopyProtection.Protection)) info.CopyProtection.Protection = seed.CopyProtection.Protection;
}
}
#endregion
#region Helpers
/// <summary>
/// Process a text block and replace with internal identifiers
/// </summary>
/// <param name="text">Text block to process</param>
/// <returns>Processed text block, if possible</returns>
private static string ReplaceHtmlWithSiteCodes(this string text)
{
if (string.IsNullOrEmpty(text))
return text;
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
var longname = siteCode.LongName();
if (!string.IsNullOrEmpty(longname))
text = text.Replace(longname, siteCode.ShortName());
}
// For some outdated tags, we need to use alternate names
text = text.Replace("<b>Demos</b>:", ((SiteCode?)SiteCode.PlayableDemos).ShortName());
text = text.Replace("DMI:", ((SiteCode?)SiteCode.DMIHash).ShortName());
text = text.Replace("<b>LucasArts ID</b>:", ((SiteCode?)SiteCode.LucasArtsID).ShortName());
text = text.Replace("PFI:", ((SiteCode?)SiteCode.PFIHash).ShortName());
text = text.Replace("SS:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("SSv1:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("<b>SSv1</b>:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("SSv2:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("<b>SSv2</b>:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("SS version:", ((SiteCode?)SiteCode.SSVersion).ShortName());
text = text.Replace("Universal Hash (SHA-1):", ((SiteCode?)SiteCode.UniversalHash).ShortName());
text = text.Replace("XeMID:", ((SiteCode?)SiteCode.XeMID).ShortName());
text = text.Replace("XMID:", ((SiteCode?)SiteCode.XMID).ShortName());
return text;
}
#endregion
}
}

View File

@@ -10,17 +10,17 @@ namespace SabreTools.RedumpLib.Data
/// <summary>
/// Regex matching the added field on a disc page
/// </summary>
public static Regex AddedRegex = new Regex(@"<tr><th>Added</th><td>(.*?)</td></tr>");
public static Regex AddedRegex = new(@"<tr><th>Added</th><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching the barcode field on a disc page
/// </summary>
public static Regex BarcodeRegex = new Regex(@"<tr><th>Barcode</th></tr><tr><td>(.*?)</td></tr>");
public static Regex BarcodeRegex = new(@"<tr><th>Barcode</th></tr><tr><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching the BCA field on a disc page
/// </summary>
public static Regex BcaRegex = new Regex(@"<h3>BCA .*?/></h3></td><td .*?></td></tr>"
public static Regex BcaRegex = new(@"<h3>BCA .*?/></h3></td><td .*?></td></tr>"
+ "<tr><th>Row</th><th>Contents</th><th>ASCII</th></tr>"
+ "<tr><td>(?<row1number>.*?)</td><td>(?<row1contents>.*?)</td><td>(?<row1ascii>.*?)</td></tr>"
+ "<tr><td>(?<row2number>.*?)</td><td>(?<row2contents>.*?)</td><td>(?<row2ascii>.*?)</td></tr>"
@@ -30,82 +30,82 @@ namespace SabreTools.RedumpLib.Data
/// <summary>
/// Regex matching the category field on a disc page
/// </summary>
public static Regex CategoryRegex = new Regex(@"<tr><th>Category</th><td>(.*?)</td></tr>");
public static Regex CategoryRegex = new(@"<tr><th>Category</th><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching the comments field on a disc page
/// </summary>
public static Regex CommentsRegex = new Regex(@"<tr><th>Comments</th></tr><tr><td>(.*?)</td></tr>", RegexOptions.Singleline);
public static Regex CommentsRegex = new(@"<tr><th>Comments</th></tr><tr><td>(.*?)</td></tr>", RegexOptions.Singleline);
/// <summary>
/// Regex matching the contents field on a disc page
/// </summary>
public static Regex ContentsRegex = new Regex(@"<tr><th>Contents</th></tr><tr .*?><td>(.*?)</td></tr>", RegexOptions.Singleline);
public static Regex ContentsRegex = new(@"<tr><th>Contents</th></tr><tr .*?><td>(.*?)</td></tr>", RegexOptions.Singleline);
/// <summary>
/// Regex matching individual disc links on a results page
/// </summary>
public static Regex DiscRegex = new Regex(@"<a href=""/disc/(\d+)/"">");
public static Regex DiscRegex = new(@"<a href=""/disc/(\d+)/"">");
/// <summary>
/// Regex matching the disc number or letter field on a disc page
/// </summary>
public static Regex DiscNumberLetterRegex = new Regex(@"\((.*?)\)");
public static Regex DiscNumberLetterRegex = new(@"\((.*?)\)");
/// <summary>
/// Regex matching the dumpers on a disc page
/// </summary>
public static Regex DumpersRegex = new Regex(@"<a href=""/discs/dumper/(.*?)/"">");
public static Regex DumpersRegex = new(@"<a href=""/discs/dumper/(.*?)/"">");
/// <summary>
/// Regex matching the edition field on a disc page
/// </summary>
public static Regex EditionRegex = new Regex(@"<tr><th>Edition</th><td>(.*?)</td></tr>");
public static Regex EditionRegex = new(@"<tr><th>Edition</th><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching the error count field on a disc page
/// </summary>
public static Regex ErrorCountRegex = new Regex(@"<tr><th>Errors count</th><td>(.*?)</td></tr>");
public static Regex ErrorCountRegex = new(@"<tr><th>Errors count</th><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching the foreign title field on a disc page
/// </summary>
public static Regex ForeignTitleRegex = new Regex(@"<h2>(.*?)</h2>");
public static Regex ForeignTitleRegex = new(@"<h2>(.*?)</h2>");
/// <summary>
/// Regex matching the "full match" ID list from a WIP disc page
/// </summary>
public static Regex FullMatchRegex = new Regex(@"<td class=""static"">full match ids: (.*?)</td>");
public static Regex FullMatchRegex = new(@"<td class=""static"">full match ids: (.*?)</td>");
/// <summary>
/// Regex matching the languages field on a disc page
/// </summary>
public static Regex LanguagesRegex = new Regex(@"<img src=""/images/languages/(.*?)\.png"" alt="".*?"" title="".*?"" />\s*");
public static Regex LanguagesRegex = new(@"<img src=""/images/languages/(.*?)\.png"" alt="".*?"" title="".*?"" />\s*");
/// <summary>
/// Regex matching the last modified field on a disc page
/// </summary>
public static Regex LastModifiedRegex = new Regex(@"<tr><th>Last modified</th><td>(.*?)</td></tr>");
public static Regex LastModifiedRegex = new(@"<tr><th>Last modified</th><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching the media field on a disc page
/// </summary>
public static Regex MediaRegex = new Regex(@"<tr><th>Media</th><td>(.*?)</td></tr>");
public static Regex MediaRegex = new(@"<tr><th>Media</th><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching individual WIP disc links on a results page
/// </summary>
public static Regex NewDiscRegex = new Regex(@"<a (style=.*)?href=""/newdisc/(\d+)/"">");
public static Regex NewDiscRegex = new(@"<a (style=.*)?href=""/newdisc/(\d+)/"">");
/// <summary>
/// Regex matching the "partial match" ID list from a WIP disc page
/// </summary>
public static Regex PartialMatchRegex = new Regex(@"<td class=""static"">partial match ids: (.*?)</td>");
public static Regex PartialMatchRegex = new(@"<td class=""static"">partial match ids: (.*?)</td>");
/// <summary>
/// Regex matching the PVD field on a disc page
/// </summary>
public static Regex PvdRegex = new Regex(@"<h3>Primary Volume Descriptor (PVD) <img .*?/></h3></td><td .*?></td></tr>"
public static Regex PvdRegex = new(@"<h3>Primary Volume Descriptor (PVD) <img .*?/></h3></td><td .*?></td></tr>"
+ @"<tr><th>Record / Entry</th><th>Contents</th><th>Date</th><th>Time</th><th>GMT</th></tr>"
+ @"<tr><td>Creation</td><td>(?<creationbytes>.*?)</td><td>(?<creationdate>.*?)</td><td>(?<creationtime>.*?)</td><td>(?<creationtimezone>.*?)</td></tr>"
+ @"<tr><td>Modification</td><td>(?<modificationbytes>.*?)</td><td>(?<modificationdate>.*?)</td><td>(?<modificationtime>.*?)</td><td>(?<modificationtimezone>.*?)</td></tr>"
@@ -115,57 +115,57 @@ namespace SabreTools.RedumpLib.Data
/// <summary>
/// Regex matching the region field on a disc page
/// </summary>
public static Regex RegionRegex = new Regex(@"<tr><th>Region</th><td><a href=""/discs/region/(.*?)/"">");
public static Regex RegionRegex = new(@"<tr><th>Region</th><td><a href=""/discs/region/(.*?)/"">");
/// <summary>
/// Regex matching a double-layer disc ringcode information
/// </summary>
public static Regex RingCodeDoubleRegex = new Regex(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould
public static Regex RingCodeDoubleRegex = new(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould
/// <summary>
/// Regex matching a single-layer disc ringcode information
/// </summary>
public static Regex RingCodeSingleRegex = new Regex(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould
public static Regex RingCodeSingleRegex = new(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould
/// <summary>
/// Regex matching the serial field on a disc page
/// </summary>
public static Regex SerialRegex = new Regex(@"<tr><th>Serial</th><td>(.*?)</td></tr>");
public static Regex SerialRegex = new(@"<tr><th>Serial</th><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching the system field on a disc page
/// </summary>
public static Regex SystemRegex = new Regex(@"<tr><th>System</th><td><a href=""/discs/system/(.*?)/"">");
public static Regex SystemRegex = new(@"<tr><th>System</th><td><a href=""/discs/system/(.*?)/"">");
/// <summary>
/// Regex matching the title field on a disc page
/// </summary>
public static Regex TitleRegex = new Regex(@"<h1>(.*?)</h1>");
public static Regex TitleRegex = new(@"<h1>(.*?)</h1>");
/// <summary>
/// Regex matching the current nonce token for login
/// </summary>
public static Regex TokenRegex = new Regex(@"<input type=""hidden"" name=""csrf_token"" value=""(.*?)"" />");
public static Regex TokenRegex = new(@"<input type=""hidden"" name=""csrf_token"" value=""(.*?)"" />");
/// <summary>
/// Regex matching a single track on a disc page
/// </summary>
public static Regex TrackRegex = new Regex(@"<tr><td>(?<number>.*?)</td><td>(?<type>.*?)</td><td>(?<pregap>.*?)</td><td>(?<length>.*?)</td><td>(?<sectors>.*?)</td><td>(?<size>.*?)</td><td>(?<crc32>.*?)</td><td>(?<md5>.*?)</td><td>(?<sha1>.*?)</td></tr>", RegexOptions.Singleline);
public static Regex TrackRegex = new(@"<tr><td>(?<number>.*?)</td><td>(?<type>.*?)</td><td>(?<pregap>.*?)</td><td>(?<length>.*?)</td><td>(?<sectors>.*?)</td><td>(?<size>.*?)</td><td>(?<crc32>.*?)</td><td>(?<md5>.*?)</td><td>(?<sha1>.*?)</td></tr>", RegexOptions.Singleline);
/// <summary>
/// Regex matching the track count on a disc page
/// </summary>
public static Regex TrackCountRegex = new Regex(@"<tr><th>Number of tracks</th><td>(.*?)</td></tr>");
public static Regex TrackCountRegex = new(@"<tr><th>Number of tracks</th><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching the version field on a disc page
/// </summary>
public static Regex VersionRegex = new Regex(@"<tr><th>Version</th><td>(.*?)</td></tr>");
public static Regex VersionRegex = new(@"<tr><th>Version</th><td>(.*?)</td></tr>");
/// <summary>
/// Regex matching the write offset field on a disc page
/// </summary>
public static Regex WriteOffsetRegex = new Regex(@"<tr><th>Write offset</th><td>(.*?)</td></tr>");
public static Regex WriteOffsetRegex = new(@"<tr><th>Write offset</th><td>(.*?)</td></tr>");
#endregion
@@ -301,6 +301,5 @@ namespace SabreTools.RedumpLib.Data
public const string Sha1Ext = "sha1/";
#endregion
}
}

View File

@@ -733,31 +733,20 @@ namespace SabreTools.RedumpLib.Data
/// <returns>DiscType if possible, null on error</returns>
public static DiscType? ToDiscType(this MediaType? mediaType)
{
switch (mediaType)
return mediaType switch
{
case MediaType.BluRay:
return DiscType.BD50;
case MediaType.CDROM:
return DiscType.CD;
case MediaType.DVD:
return DiscType.DVD9;
case MediaType.GDROM:
return DiscType.GDROM;
case MediaType.HDDVD:
return DiscType.HDDVDSL;
// case MediaType.MILCD: // TODO: Support this?
// return DiscType.MILCD;
case MediaType.NintendoGameCubeGameDisc:
return DiscType.NintendoGameCubeGameDisc;
case MediaType.NintendoWiiOpticalDisc:
return DiscType.NintendoWiiOpticalDiscDL;
case MediaType.NintendoWiiUOpticalDisc:
return DiscType.NintendoWiiUOpticalDiscSL;
case MediaType.UMD:
return DiscType.UMDDL;
default:
return null;
}
MediaType.BluRay => DiscType.BD50,
MediaType.CDROM => DiscType.CD,
MediaType.DVD => DiscType.DVD9,
MediaType.GDROM => DiscType.GDROM,
MediaType.HDDVD => DiscType.HDDVDSL,
// MediaType.MILCD => DiscType.MILCD, // TODO: Support this?
MediaType.NintendoGameCubeGameDisc => DiscType.NintendoGameCubeGameDisc,
MediaType.NintendoWiiOpticalDisc => DiscType.NintendoWiiOpticalDiscDL,
MediaType.NintendoWiiUOpticalDisc => DiscType.NintendoWiiUOpticalDiscSL,
MediaType.UMD => DiscType.UMDDL,
_ => null,
};
}
/// <summary>
@@ -767,40 +756,29 @@ namespace SabreTools.RedumpLib.Data
/// <returns>MediaType if possible, null on error</returns>
public static MediaType? ToMediaType(this DiscType? discType)
{
switch (discType)
return discType switch
{
case DiscType.BD25:
case DiscType.BD33:
case DiscType.BD50:
case DiscType.BD66:
case DiscType.BD100:
case DiscType.BD128:
return MediaType.BluRay;
case DiscType.CD:
return MediaType.CDROM;
case DiscType.DVD5:
case DiscType.DVD9:
return MediaType.DVD;
case DiscType.GDROM:
return MediaType.GDROM;
case DiscType.HDDVDSL:
case DiscType.HDDVDDL:
return MediaType.HDDVD;
// case DiscType.MILCD: // TODO: Support this?
// return MediaType.MILCD;
case DiscType.NintendoGameCubeGameDisc:
return MediaType.NintendoGameCubeGameDisc;
case DiscType.NintendoWiiOpticalDiscSL:
case DiscType.NintendoWiiOpticalDiscDL:
return MediaType.NintendoWiiOpticalDisc;
case DiscType.NintendoWiiUOpticalDiscSL:
return MediaType.NintendoWiiUOpticalDisc;
case DiscType.UMDSL:
case DiscType.UMDDL:
return MediaType.UMD;
default:
return null;
}
DiscType.BD25
or DiscType.BD33
or DiscType.BD50
or DiscType.BD66
or DiscType.BD100
or DiscType.BD128 => MediaType.BluRay,
DiscType.CD => MediaType.CDROM,
DiscType.DVD5
or DiscType.DVD9 => MediaType.DVD,
DiscType.GDROM => MediaType.GDROM,
DiscType.HDDVDSL
or DiscType.HDDVDDL => MediaType.HDDVD,
// DiscType.MILCD => MediaType.MILCD, // TODO: Support this?
DiscType.NintendoGameCubeGameDisc => MediaType.NintendoGameCubeGameDisc,
DiscType.NintendoWiiOpticalDiscSL
or DiscType.NintendoWiiOpticalDiscDL => MediaType.NintendoWiiOpticalDisc,
DiscType.NintendoWiiUOpticalDiscSL => MediaType.NintendoWiiUOpticalDisc,
DiscType.UMDSL
or DiscType.UMDDL => MediaType.UMD,
_ => null,
};
}
#endregion
@@ -821,35 +799,23 @@ namespace SabreTools.RedumpLib.Data
/// <returns>Category represented by the string, if possible</returns>
public static DiscCategory? ToDiscCategory(string category)
{
switch (category?.ToLowerInvariant())
return (category?.ToLowerInvariant()) switch
{
case "games":
return DiscCategory.Games;
case "demos":
return DiscCategory.Demos;
case "video":
return DiscCategory.Video;
case "audio":
return DiscCategory.Audio;
case "multimedia":
return DiscCategory.Multimedia;
case "applications":
return DiscCategory.Applications;
case "coverdiscs":
return DiscCategory.Coverdiscs;
case "educational":
return DiscCategory.Educational;
case "bonusdiscs":
case "bonus discs":
return DiscCategory.BonusDiscs;
case "preproduction":
return DiscCategory.Preproduction;
case "addons":
case "add-ons":
return DiscCategory.AddOns;
default:
return DiscCategory.Games;
}
"games" => (DiscCategory?)DiscCategory.Games,
"demos" => (DiscCategory?)DiscCategory.Demos,
"video" => (DiscCategory?)DiscCategory.Video,
"audio" => (DiscCategory?)DiscCategory.Audio,
"multimedia" => (DiscCategory?)DiscCategory.Multimedia,
"applications" => (DiscCategory?)DiscCategory.Applications,
"coverdiscs" => (DiscCategory?)DiscCategory.Coverdiscs,
"educational" => (DiscCategory?)DiscCategory.Educational,
"bonusdiscs"
or "bonus discs" => (DiscCategory?)DiscCategory.BonusDiscs,
"preproduction" => (DiscCategory?)DiscCategory.Preproduction,
"addons"
or "add-ons" => (DiscCategory?)DiscCategory.AddOns,
_ => (DiscCategory?)DiscCategory.Games,
};
}
#endregion
@@ -870,72 +836,52 @@ namespace SabreTools.RedumpLib.Data
/// <returns>DiscType represented by the string, if possible</returns>
public static DiscType? ToDiscType(string discType)
{
switch (discType?.ToLowerInvariant())
return (discType?.ToLowerInvariant()) switch
{
case "bd25":
case "bd-25":
return DiscType.BD25;
case "bd33":
case "bd-33":
return DiscType.BD33;
case "bd50":
case "bd-50":
return DiscType.BD50;
case "bd66":
case "bd-66":
return DiscType.BD66;
case "bd100":
case "bd-100":
return DiscType.BD100;
case "bd128":
case "bd-128":
return DiscType.BD128;
case "cd":
case "cdrom":
case "cd-rom":
return DiscType.CD;
case "dvd5":
case "dvd-5":
return DiscType.DVD5;
case "dvd9":
case "dvd-9":
return DiscType.DVD9;
case "gd":
case "gdrom":
case "gd-rom":
return DiscType.GDROM;
case "hddvd":
case "hddvdsl":
case "hd-dvd sl":
return DiscType.HDDVDSL;
case "hddvddl":
case "hd-dvd dl":
return DiscType.HDDVDDL;
case "milcd":
case "mil-cd":
return DiscType.MILCD;
case "nintendogamecubegamedisc":
case "nintendo game cube game disc":
return DiscType.NintendoGameCubeGameDisc;
case "nintendowiiopticaldiscsl":
case "nintendo wii optical disc sl":
return DiscType.NintendoWiiOpticalDiscSL;
case "nintendowiiopticaldiscdl":
case "nintendo wii optical disc dl":
return DiscType.NintendoWiiOpticalDiscDL;
case "nintendowiiuopticaldiscsl":
case "nintendo wii u optical disc sl":
return DiscType.NintendoWiiUOpticalDiscSL;
case "umd":
case "umdsl":
case "umd sl":
return DiscType.UMDSL;
case "umddl":
case "umd dl":
return DiscType.UMDDL;
default:
return null;
}
"bd25"
or "bd-25" => (DiscType?)DiscType.BD25,
"bd33"
or "bd-33" => (DiscType?)DiscType.BD33,
"bd50"
or "bd-50" => (DiscType?)DiscType.BD50,
"bd66"
or "bd-66" => (DiscType?)DiscType.BD66,
"bd100"
or "bd-100" => (DiscType?)DiscType.BD100,
"bd128"
or "bd-128" => (DiscType?)DiscType.BD128,
"cd"
or "cdrom"
or "cd-rom" => (DiscType?)DiscType.CD,
"dvd5"
or "dvd-5" => (DiscType?)DiscType.DVD5,
"dvd9"
or "dvd-9" => (DiscType?)DiscType.DVD9,
"gd"
or "gdrom"
or "gd-rom" => (DiscType?)DiscType.GDROM,
"hddvd"
or "hddvdsl"
or "hd-dvd sl" => (DiscType?)DiscType.HDDVDSL,
"hddvddl"
or "hd-dvd dl" => (DiscType?)DiscType.HDDVDDL,
"milcd"
or "mil-cd" => (DiscType?)DiscType.MILCD,
"nintendogamecubegamedisc"
or "nintendo game cube game disc" => (DiscType?)DiscType.NintendoGameCubeGameDisc,
"nintendowiiopticaldiscsl"
or "nintendo wii optical disc sl" => (DiscType?)DiscType.NintendoWiiOpticalDiscSL,
"nintendowiiopticaldiscdl"
or "nintendo wii optical disc dl" => (DiscType?)DiscType.NintendoWiiOpticalDiscDL,
"nintendowiiuopticaldiscsl"
or "nintendo wii u optical disc sl" => (DiscType?)DiscType.NintendoWiiUOpticalDiscSL,
"umd"
or "umdsl"
or "umd sl" => (DiscType?)DiscType.UMDSL,
"umddl"
or "umd dl" => (DiscType?)DiscType.UMDDL,
_ => null,
};
}
#endregion
@@ -957,19 +903,16 @@ namespace SabreTools.RedumpLib.Data
public static string? ShortName(this Language? language)
{
// Some languages need to use the alternate code instead
switch (language)
return language switch
{
case Language.Albanian:
case Language.Armenian:
case Language.Icelandic:
case Language.Macedonian:
case Language.Romanian:
case Language.Slovak:
return language.ThreeLetterCodeAlt();
default:
return language.ThreeLetterCode();
}
Language.Albanian
or Language.Armenian
or Language.Icelandic
or Language.Macedonian
or Language.Romanian
or Language.Slovak => language.ThreeLetterCodeAlt(),
_ => language.ThreeLetterCode(),
};
}
/// <summary>
@@ -1119,6 +1062,31 @@ namespace SabreTools.RedumpLib.Data
#region Site Code
/// <summary>
/// Check if a site code is multi-line or not
/// </summary>
/// <param name="siteCode">SiteCode to check</param>
/// <returns>True if the code field is multiline by default, false otherwise</returns>
public static bool IsMultiLine(this SiteCode? siteCode)
{
return siteCode switch
{
SiteCode.Extras => true,
SiteCode.Filename => true,
SiteCode.Games => true,
SiteCode.GameFootage => true,
SiteCode.Multisession => true,
SiteCode.NetYarozeGames => true,
SiteCode.Patches => true,
SiteCode.PlayableDemos => true,
SiteCode.RollingDemos => true,
SiteCode.Savegames => true,
SiteCode.TechDemos => true,
SiteCode.Videos => true,
_ => false,
};
}
/// <summary>
/// Get the HTML version for each known site code
/// </summary>
@@ -1137,6 +1105,79 @@ namespace SabreTools.RedumpLib.Data
#region System
/// <summary>
/// Determine if a system is okay if it's not detected by Windows
/// </summary>
/// <param name="system">RedumpSystem value to check</param>
/// <returns>True if Windows show see a disc when dumping, false otherwise</returns>
public static bool DetectedByWindows(this RedumpSystem? system)
{
return system switch
{
RedumpSystem.AmericanLaserGames3DO
or RedumpSystem.AppleMacintosh
or RedumpSystem.Atari3DO
or RedumpSystem.AtariJaguarCDInteractiveMultimediaSystem
or RedumpSystem.NewJatreCDi
or RedumpSystem.NintendoGameCube
or RedumpSystem.NintendoWii
or RedumpSystem.NintendoWiiU
or RedumpSystem.PhilipsCDi
or RedumpSystem.PhilipsCDiDigitalVideo
or RedumpSystem.Panasonic3DOInteractiveMultiplayer
or RedumpSystem.PanasonicM2
or RedumpSystem.PioneerLaserActive
or RedumpSystem.SuperAudioCD => false,
_ => true,
};
}
/// <summary>
/// Determine if a system has reversed ringcodes
/// </summary>
/// <param name="system">RedumpSystem value to check</param>
/// <returns>True if the system has reversed ringcodes, false otherwise</returns>
public static bool HasReversedRingcodes(this RedumpSystem? system)
{
return system switch
{
RedumpSystem.SonyPlayStation2
or RedumpSystem.SonyPlayStation3
or RedumpSystem.SonyPlayStation4
or RedumpSystem.SonyPlayStation5
or RedumpSystem.SonyPlayStationPortable => true,
_ => false,
};
}
/// <summary>
/// Determine if a system is considered audio-only
/// </summary>
/// <param name="system">RedumpSystem value to check</param>
/// <returns>True if the system is audio-only, false otherwise</returns>
/// <remarks>
/// Philips CD-i should NOT be in this list. It's being included until there's a
/// reasonable distinction between CD-i and CD-i ready on the database side.
/// </remarks>
public static bool IsAudio(this RedumpSystem? system)
{
return system switch
{
RedumpSystem.AtariJaguarCDInteractiveMultimediaSystem
or RedumpSystem.AudioCD
or RedumpSystem.DVDAudio
or RedumpSystem.HasbroiONEducationalGamingSystem
or RedumpSystem.HasbroVideoNow
or RedumpSystem.HasbroVideoNowColor
or RedumpSystem.HasbroVideoNowJr
or RedumpSystem.HasbroVideoNowXP
or RedumpSystem.PhilipsCDi
or RedumpSystem.PlayStationGameSharkUpdates
or RedumpSystem.SuperAudioCD => true,
_ => false,
};
}
/// <summary>
/// Determine if a system is a marker value
/// </summary>
@@ -1144,17 +1185,31 @@ namespace SabreTools.RedumpLib.Data
/// <returns>True if the system is a marker value, false otherwise</returns>
public static bool IsMarker(this RedumpSystem? system)
{
switch (system)
return system switch
{
case RedumpSystem.MarkerArcadeEnd:
case RedumpSystem.MarkerComputerEnd:
case RedumpSystem.MarkerDiscBasedConsoleEnd:
// case RedumpSystem.MarkerOtherConsoleEnd:
case RedumpSystem.MarkerOtherEnd:
return true;
default:
return false;
}
RedumpSystem.MarkerArcadeEnd
or RedumpSystem.MarkerComputerEnd
or RedumpSystem.MarkerDiscBasedConsoleEnd
or RedumpSystem.MarkerOtherEnd => true,
_ => false,
};
}
/// <summary>
/// Determine if a system is considered XGD
/// </summary>
/// <param name="system">RedumpSystem value to check</param>
/// <returns>True if the system is XGD, false otherwise</returns>
public static bool IsXGD(this RedumpSystem? system)
{
return system switch
{
RedumpSystem.MicrosoftXbox
or RedumpSystem.MicrosoftXbox360
or RedumpSystem.MicrosoftXboxOne
or RedumpSystem.MicrosoftXboxSeriesXS => true,
_ => false,
};
}
/// <summary>
@@ -2157,15 +2212,12 @@ namespace SabreTools.RedumpLib.Data
/// <returns>YesNo represented by the nullable boolean, if possible</returns>
public static YesNo? ToYesNo(this bool? yesno)
{
switch (yesno)
return yesno switch
{
case false:
return YesNo.No;
case true:
return YesNo.Yes;
default:
return YesNo.NULL;
}
false => YesNo.No,
true => YesNo.Yes,
_ => YesNo.NULL,
};
}
/// <summary>
@@ -2175,15 +2227,12 @@ namespace SabreTools.RedumpLib.Data
/// <returns>YesNo represented by the string, if possible</returns>
public static YesNo? ToYesNo(string yesno)
{
switch (yesno?.ToLowerInvariant())
return (yesno?.ToLowerInvariant()) switch
{
case "no":
return YesNo.No;
case "yes":
return YesNo.Yes;
default:
return YesNo.NULL;
}
"no" => YesNo.No,
"yes" => YesNo.Yes,
_ => YesNo.NULL,
};
}
#endregion

View File

@@ -69,7 +69,7 @@ namespace SabreTools.RedumpLib.Data
public DumpingInfoSection? DumpingInfo { get; set; } = new DumpingInfoSection();
[JsonProperty(PropertyName = "artifacts", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, string>? Artifacts { get; set; } = new Dictionary<string, string>();
public Dictionary<string, string>? Artifacts { get; set; } = [];
public object Clone()
{

72
Data/Template.cs Normal file
View File

@@ -0,0 +1,72 @@
namespace SabreTools.RedumpLib.Data
{
/// <summary>
/// Template field values for submission info
/// </summary>
internal static class Template
{
// Manual information
public const string TitleField = "Title";
public const string ForeignTitleField = "Foreign Title (Non-latin)";
public const string DiscNumberField = "Disc Number / Letter";
public const string DiscTitleField = "Disc Title";
public const string SystemField = "System";
public const string MediaTypeField = "Media Type";
public const string CategoryField = "Category";
public const string RegionField = "Region";
public const string LanguagesField = "Languages";
public const string PlaystationLanguageSelectionViaField = "Language Selection Via";
public const string DiscSerialField = "Disc Serial";
public const string BarcodeField = "Barcode";
public const string CommentsField = "Comments";
public const string ContentsField = "Contents";
public const string VersionField = "Version";
public const string EditionField = "Edition/Release";
public const string PlayStation3WiiDiscKeyField = "Disc Key";
public const string PlayStation3DiscIDField = "Disc ID";
public const string GameCubeWiiBCAField = "BCA";
public const string CopyProtectionField = "Copy Protection";
public const string MasteringRingField = "Mastering Code (laser branded/etched)";
public const string MasteringSIDField = "Mastering SID Code";
public const string MouldSIDField = "Mould SID Code";
public const string AdditionalMouldField = "Additional Mould";
public const string ToolstampField = "Toolstamp or Mastering Code (engraved/stamped)";
// Automatic Information
public const string DumpingProgramField = "Dumping Program";
public const string DumpingDateField = "Date";
public const string DumpingDriveManufacturer = "Manufacturer";
public const string DumpingDriveModel = "Model";
public const string DumpingDriveFirmware = "Firmware";
public const string ReportedDiscType = "Reported Disc Type";
public const string PVDField = "Primary Volume Descriptor (PVD)";
public const string DATField = "DAT";
public const string SizeField = "Size";
public const string CRC32Field = "CRC32";
public const string MD5Field = "MD5";
public const string SHA1Field = "SHA1";
public const string FullyMatchingIDField = "Fully Matching ID";
public const string PartiallyMatchingIDsField = "Partially Matching IDs";
public const string ErrorCountField = "Error Count";
public const string CuesheetField = "Cuesheet";
public const string SubIntentionField = "SubIntention Data (SecuROM/LibCrypt)";
public const string WriteOffsetField = "Write Offset";
public const string LayerbreakField = "Layerbreak";
public const string EXEDateBuildDate = "EXE/Build Date";
public const string HeaderField = "Header";
public const string PICField = "Permanent Information & Control (PIC)";
public const string PlayStationEDCField = "EDC";
public const string PlayStationAntiModchipField = "Anti-modchip";
public const string PlayStationLibCryptField = "LibCrypt";
public const string XBOXSSRanges = "Security Sector Ranges";
// Default values
public const string RequiredValue = "(REQUIRED)";
public const string RequiredIfExistsValue = "(REQUIRED, IF EXISTS)";
public const string OptionalValue = "(OPTIONAL)";
public const string DiscNotDetected = "Disc Not Detected";
}
}

685
Formatter.cs Normal file
View File

@@ -0,0 +1,685 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib
{
public static class Formatter
{
/// <summary>
/// Format the output data in a human readable way, separating each printed line into a new item in the list
/// </summary>
/// <param name="info">Information object that should contain normalized values</param>
/// <param name="enableRedumpCompatibility">True to enable Redump compatiblity, false otherwise</param>
/// <returns>List of strings representing each line of an output file, null on error</returns>
public static (List<string>?, string?) FormatOutputData(SubmissionInfo? info, bool enableRedumpCompatibility)
{
// Check to see if the inputs are valid
if (info == null)
return (null, "Submission information was missing");
try
{
// Sony-printed discs have layers in the opposite order
var system = info.CommonDiscInfo?.System;
bool reverseOrder = system.HasReversedRingcodes();
// Preamble for submission
#pragma warning disable IDE0028
var output = new List<string>
{
"Users who wish to submit this information to Redump must ensure that all of the fields below are accurate for the exact media they have.",
"Please double-check to ensure that there are no fields that need verification, such as the version or copy protection.",
"If there are no fields in need of verification or all fields are accurate, this preamble can be removed before submission.",
"",
};
// Common Disc Info section
output.Add("Common Disc Info:");
AddIfExists(output, Template.TitleField, info.CommonDiscInfo?.Title, 1);
AddIfExists(output, Template.ForeignTitleField, info.CommonDiscInfo?.ForeignTitleNonLatin, 1);
AddIfExists(output, Template.DiscNumberField, info.CommonDiscInfo?.DiscNumberLetter, 1);
AddIfExists(output, Template.DiscTitleField, info.CommonDiscInfo?.DiscTitle, 1);
AddIfExists(output, Template.SystemField, info.CommonDiscInfo?.System.LongName(), 1);
AddIfExists(output, Template.MediaTypeField, GetFixedMediaType(
info.CommonDiscInfo?.Media.ToMediaType(),
info.SizeAndChecksums?.PICIdentifier,
info.SizeAndChecksums?.Size,
info.SizeAndChecksums?.Layerbreak,
info.SizeAndChecksums?.Layerbreak2,
info.SizeAndChecksums?.Layerbreak3),
1);
AddIfExists(output, Template.CategoryField, info.CommonDiscInfo?.Category.LongName(), 1);
AddIfExists(output, Template.FullyMatchingIDField, info.FullyMatchedID?.ToString(), 1);
AddIfExists(output, Template.PartiallyMatchingIDsField, info.PartiallyMatchedIDs, 1);
AddIfExists(output, Template.RegionField, info.CommonDiscInfo?.Region.LongName() ?? "SPACE! (CHANGE THIS)", 1);
AddIfExists(output, Template.LanguagesField, (info.CommonDiscInfo?.Languages ?? [null]).Select(l => l.LongName() ?? "SILENCE! (CHANGE THIS)").ToArray(), 1);
AddIfExists(output, Template.PlaystationLanguageSelectionViaField, (info.CommonDiscInfo?.LanguageSelection ?? []).Select(l => l.LongName()).ToArray(), 1);
AddIfExists(output, Template.DiscSerialField, info.CommonDiscInfo?.Serial, 1);
// All ringcode information goes in an indented area
output.Add(""); output.Add("\tRingcode Information:"); output.Add("");
// If we have a triple-layer disc
if (info.SizeAndChecksums?.Layerbreak3 != default && info.SizeAndChecksums?.Layerbreak3 != default(long))
{
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer0MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer0MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer0ToolstampMasteringCode, 0);
AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer0MouldSID, 0);
AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer0AdditionalMould, 0);
AddIfExists(output, "Layer 1 " + Template.MasteringRingField, info.CommonDiscInfo?.Layer1MasteringRing, 0);
AddIfExists(output, "Layer 1 " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer1MasteringSID, 0);
AddIfExists(output, "Layer 1 " + Template.ToolstampField, info.CommonDiscInfo?.Layer1ToolstampMasteringCode, 0);
AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer1MouldSID, 0);
AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer1AdditionalMould, 0);
AddIfExists(output, "Layer 2 " + Template.MasteringRingField, info.CommonDiscInfo?.Layer2MasteringRing, 0);
AddIfExists(output, "Layer 2 " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer2MasteringSID, 0);
AddIfExists(output, "Layer 2 " + Template.ToolstampField, info.CommonDiscInfo?.Layer2ToolstampMasteringCode, 0);
AddIfExists(output, (reverseOrder ? "Layer 3 (Inner) " : "Layer 3 (Outer) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer3MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 3 (Inner) " : "Layer 3 (Outer) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer3MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 3 (Inner) " : "Layer 3 (Outer) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer3ToolstampMasteringCode, 0);
}
// If we have a triple-layer disc
else if (info.SizeAndChecksums?.Layerbreak2 != default && info.SizeAndChecksums?.Layerbreak2 != default(long))
{
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer0MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer0MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer0ToolstampMasteringCode, 0);
AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer0MouldSID, 0);
AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer0AdditionalMould, 0);
AddIfExists(output, "Layer 1 " + Template.MasteringRingField, info.CommonDiscInfo?.Layer1MasteringRing, 0);
AddIfExists(output, "Layer 1 " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer1MasteringSID, 0);
AddIfExists(output, "Layer 1 " + Template.ToolstampField, info.CommonDiscInfo?.Layer1ToolstampMasteringCode, 0);
AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer1MouldSID, 0);
AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer1AdditionalMould, 0);
AddIfExists(output, (reverseOrder ? "Layer 2 (Inner) " : "Layer 2 (Outer) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer2MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 2 (Inner) " : "Layer 2 (Outer) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer2MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 2 (Inner) " : "Layer 2 (Outer) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer2ToolstampMasteringCode, 0);
}
// If we have a dual-layer disc
else if (info.SizeAndChecksums?.Layerbreak != default && info.SizeAndChecksums?.Layerbreak != default(long))
{
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer0MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer0MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer0ToolstampMasteringCode, 0);
AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer0MouldSID, 0);
AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer0AdditionalMould, 0);
AddIfExists(output, (reverseOrder ? "Layer 1 (Inner) " : "Layer 1 (Outer) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer1MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 1 (Inner) " : "Layer 1 (Outer) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer1MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 1 (Inner) " : "Layer 1 (Outer) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer1ToolstampMasteringCode, 0);
AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer1MouldSID, 0);
AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer1AdditionalMould, 0);
}
// If we have a single-layer disc
else
{
AddIfExists(output, "Data Side " + Template.MasteringRingField, info.CommonDiscInfo?.Layer0MasteringRing, 0);
AddIfExists(output, "Data Side " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer0MasteringSID, 0);
AddIfExists(output, "Data Side " + Template.ToolstampField, info.CommonDiscInfo?.Layer0ToolstampMasteringCode, 0);
AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer0MouldSID, 0);
AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer0AdditionalMould, 0);
AddIfExists(output, "Label Side " + Template.MasteringRingField, info.CommonDiscInfo?.Layer1MasteringRing, 0);
AddIfExists(output, "Label Side " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer1MasteringSID, 0);
AddIfExists(output, "Label Side " + Template.ToolstampField, info.CommonDiscInfo?.Layer1ToolstampMasteringCode, 0);
AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer1MouldSID, 0);
AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer1AdditionalMould, 0);
}
output.Add("");
AddIfExists(output, Template.BarcodeField, info.CommonDiscInfo?.Barcode, 1);
AddIfExists(output, Template.EXEDateBuildDate, info.CommonDiscInfo?.EXEDateBuildDate, 1);
AddIfExists(output, Template.ErrorCountField, info.CommonDiscInfo?.ErrorsCount, 1);
AddIfExists(output, Template.CommentsField, info.CommonDiscInfo?.Comments?.Trim(), 1);
AddIfExists(output, Template.ContentsField, info.CommonDiscInfo?.Contents?.Trim(), 1);
// Version and Editions section
output.Add(""); output.Add("Version and Editions:");
AddIfExists(output, Template.VersionField, info.VersionAndEditions?.Version, 1);
AddIfExists(output, Template.EditionField, info.VersionAndEditions?.OtherEditions, 1);
// EDC section
if (info.CommonDiscInfo?.System == RedumpSystem.SonyPlayStation)
{
output.Add(""); output.Add("EDC:");
AddIfExists(output, Template.PlayStationEDCField, info.EDC?.EDC.LongName(), 1);
}
// Parent/Clone Relationship section
// output.Add(""); output.Add("Parent/Clone Relationship:");
// AddIfExists(output, Template.ParentIDField, info.ParentID);
// AddIfExists(output, Template.RegionalParentField, info.RegionalParent.ToString());
// Extras section
if (info.Extras?.PVD != null || info.Extras?.PIC != null || info.Extras?.BCA != null || info.Extras?.SecuritySectorRanges != null)
{
output.Add(""); output.Add("Extras:");
AddIfExists(output, Template.PVDField, info.Extras.PVD?.Trim(), 1);
AddIfExists(output, Template.PlayStation3WiiDiscKeyField, info.Extras.DiscKey, 1);
AddIfExists(output, Template.PlayStation3DiscIDField, info.Extras.DiscID, 1);
AddIfExists(output, Template.PICField, info.Extras.PIC, 1);
AddIfExists(output, Template.HeaderField, info.Extras.Header, 1);
AddIfExists(output, Template.GameCubeWiiBCAField, info.Extras.BCA, 1);
AddIfExists(output, Template.XBOXSSRanges, info.Extras.SecuritySectorRanges, 1);
}
// Copy Protection section
if (!string.IsNullOrEmpty(info.CopyProtection?.Protection)
|| (info.CopyProtection?.AntiModchip != null && info.CopyProtection.AntiModchip != YesNo.NULL)
|| (info.CopyProtection?.LibCrypt != null && info.CopyProtection.LibCrypt != YesNo.NULL)
|| !string.IsNullOrEmpty(info.CopyProtection?.LibCryptData)
|| !string.IsNullOrEmpty(info.CopyProtection?.SecuROMData))
{
output.Add(""); output.Add("Copy Protection:");
if (info.CommonDiscInfo?.System == RedumpSystem.SonyPlayStation)
{
AddIfExists(output, Template.PlayStationAntiModchipField, info.CopyProtection!.AntiModchip.LongName(), 1);
AddIfExists(output, Template.PlayStationLibCryptField, info.CopyProtection.LibCrypt.LongName(), 1);
AddIfExists(output, Template.SubIntentionField, info.CopyProtection.LibCryptData, 1);
}
AddIfExists(output, Template.CopyProtectionField, info.CopyProtection!.Protection, 1);
AddIfExists(output, Template.SubIntentionField, info.CopyProtection.SecuROMData, 1);
}
// Dumpers and Status section
// output.Add(""); output.Add("Dumpers and Status");
// AddIfExists(output, Template.StatusField, info.Status.Name());
// AddIfExists(output, Template.OtherDumpersField, info.OtherDumpers);
// Tracks and Write Offsets section
if (!string.IsNullOrEmpty(info.TracksAndWriteOffsets?.ClrMameProData))
{
output.Add(""); output.Add("Tracks and Write Offsets:");
AddIfExists(output, Template.DATField, info.TracksAndWriteOffsets!.ClrMameProData + "\n", 1);
AddIfExists(output, Template.CuesheetField, info.TracksAndWriteOffsets.Cuesheet, 1);
var offset = info.TracksAndWriteOffsets.OtherWriteOffsets;
if (Int32.TryParse(offset, out int i))
offset = i.ToString("+#;-#;0");
AddIfExists(output, Template.WriteOffsetField, offset, 1);
}
// Size & Checksum section
else
{
output.Add(""); output.Add("Size & Checksum:");
// Gross hack because of automatic layerbreaks in Redump
if (!enableRedumpCompatibility
|| (info.CommonDiscInfo?.Media.ToMediaType() != MediaType.BluRay
&& info.CommonDiscInfo?.System.IsXGD() == false))
{
AddIfExists(output, Template.LayerbreakField, info.SizeAndChecksums?.Layerbreak, 1);
}
AddIfExists(output, Template.SizeField, info.SizeAndChecksums?.Size.ToString(), 1);
AddIfExists(output, Template.CRC32Field, info.SizeAndChecksums?.CRC32, 1);
AddIfExists(output, Template.MD5Field, info.SizeAndChecksums?.MD5, 1);
AddIfExists(output, Template.SHA1Field, info.SizeAndChecksums?.SHA1, 1);
}
// Dumping Info section
output.Add(""); output.Add("Dumping Info:");
AddIfExists(output, Template.DumpingProgramField, info.DumpingInfo?.DumpingProgram, 1);
AddIfExists(output, Template.DumpingDateField, info.DumpingInfo?.DumpingDate, 1);
AddIfExists(output, Template.DumpingDriveManufacturer, info.DumpingInfo?.Manufacturer, 1);
AddIfExists(output, Template.DumpingDriveModel, info.DumpingInfo?.Model, 1);
AddIfExists(output, Template.DumpingDriveFirmware, info.DumpingInfo?.Firmware, 1);
AddIfExists(output, Template.ReportedDiscType, info.DumpingInfo?.ReportedDiscType, 1);
// Make sure there aren't any instances of two blank lines in a row
string? last = null;
for (int i = 0; i < output.Count;)
{
if (output[i] == last && string.IsNullOrEmpty(last))
{
output.RemoveAt(i);
}
else
{
last = output[i];
i++;
}
}
return (output, "Formatting complete!");
}
catch (Exception ex)
{
return (null, $"Error formatting submission info: {ex}");
}
}
/// <summary>
/// Process any fields that have to be combined
/// </summary>
/// <param name="info">Information object to normalize</param>
public static void ProcessSpecialFields(SubmissionInfo? info)
{
// If there is no submission info
if (info == null)
return;
// Process the comments field
if (info.CommonDiscInfo?.CommentsSpecialFields != null && info.CommonDiscInfo.CommentsSpecialFields?.Any() == true)
{
// If the field is missing, add an empty one to fill in
if (info.CommonDiscInfo.Comments == null)
info.CommonDiscInfo.Comments = string.Empty;
// Add all special fields before any comments
info.CommonDiscInfo.Comments = string.Join(
"\n", OrderCommentTags(info.CommonDiscInfo.CommentsSpecialFields)
.Where(kvp => !string.IsNullOrEmpty(kvp.Value))
.Select(FormatSiteTag)
.Where(s => !string.IsNullOrEmpty(s))
.ToArray()
) + "\n" + info.CommonDiscInfo.Comments;
// Normalize newlines
info.CommonDiscInfo.Comments = info.CommonDiscInfo.Comments.Replace("\r\n", "\n");
// Trim the comments field
info.CommonDiscInfo.Comments = info.CommonDiscInfo.Comments.Trim();
// Wipe out the special fields dictionary
info.CommonDiscInfo.CommentsSpecialFields = null;
}
// Process the contents field
if (info.CommonDiscInfo?.ContentsSpecialFields != null && info.CommonDiscInfo.ContentsSpecialFields?.Any() == true)
{
// If the field is missing, add an empty one to fill in
if (info.CommonDiscInfo.Contents == null)
info.CommonDiscInfo.Contents = string.Empty;
// Add all special fields before any contents
info.CommonDiscInfo.Contents = string.Join(
"\n", OrderContentTags(info.CommonDiscInfo.ContentsSpecialFields)
.Where(kvp => !string.IsNullOrEmpty(kvp.Value))
.Select(FormatSiteTag)
.Where(s => !string.IsNullOrEmpty(s))
.ToArray()
) + "\n" + info.CommonDiscInfo.Contents;
// Normalize newlines
info.CommonDiscInfo.Contents = info.CommonDiscInfo.Contents.Replace("\r\n", "\n");
// Trim the contents field
info.CommonDiscInfo.Contents = info.CommonDiscInfo.Contents.Trim();
// Wipe out the special fields dictionary
info.CommonDiscInfo.ContentsSpecialFields = null;
}
}
#region Helpers
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
private static void AddIfExists(List<string> output, string key, string? value, int indent)
{
// If there's no valid value to write
if (value == null)
return;
string prefix = string.Empty;
for (int i = 0; i < indent; i++)
prefix += "\t";
// Skip fields that need to keep internal whitespace intact
if (key != "Primary Volume Descriptor (PVD)"
&& key != "Header"
&& key != "Cuesheet")
{
// Convert to tabs
value = value.Replace("<tab>", "\t");
value = value.Replace("<TAB>", "\t");
value = value.Replace(" ", "\t");
// Sanitize whitespace around tabs
value = Regex.Replace(value, @"\s*\t\s*", "\t", RegexOptions.Compiled);
}
// If the value contains a newline
value = value.Replace("\r\n", "\n");
if (value.Contains('\n'))
{
output.Add(prefix + key + ":"); output.Add("");
string[] values = value.Split('\n');
foreach (string val in values)
output.Add(val);
output.Add("");
}
// For all regular values
else
{
output.Add(prefix + key + ": " + value);
}
}
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
private static void AddIfExists(List<string> output, string key, string?[]? value, int indent)
{
// If there's no valid value to write
if (value == null || value.Length == 0)
return;
AddIfExists(output, key, string.Join(", ", value), indent);
}
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
private static void AddIfExists(List<string> output, string key, long? value, int indent)
{
// If there's no valid value to write
if (value == null || value == default(long))
return;
string prefix = string.Empty;
for (int i = 0; i < indent; i++)
prefix += "\t";
output.Add(prefix + key + ": " + value);
}
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
private static void AddIfExists(List<string> output, string key, List<int>? value, int indent)
{
// If there's no valid value to write
if (value == null || value.Count == 0)
return;
AddIfExists(output, key, string.Join(", ", value.Select(o => o.ToString()).ToArray()), indent);
}
/// <summary>
/// Format a single site tag to string
/// </summary>
/// <param name="kvp">KeyValuePair representing the site tag and value</param>
/// <returns>String-formatted tag and value</returns>
private static string FormatSiteTag(KeyValuePair<SiteCode?, string> kvp)
{
bool isMultiLine = kvp.Key.IsMultiLine();
string line = $"{kvp.Key.ShortName()}{(isMultiLine ? "\n" : " ")}";
// Special case for boolean fields
if (IsBoolean(kvp.Key))
{
if (kvp.Value != true.ToString())
return string.Empty;
return line.Trim();
}
return $"{line}{kvp.Value}{(isMultiLine ? "\n" : string.Empty)}";
}
/// <summary>
/// Get the adjusted name of the media based on layers, if applicable
/// </summary>
/// <param name="mediaType">MediaType to get the proper name for</param>
/// <param name="picIdentifier">PIC identifier string (BD only)</param>
/// <param name="size">Size of the current media</param>
/// <param name="layerbreak">First layerbreak value, as applicable</param>
/// <param name="layerbreak2">Second layerbreak value, as applicable</param>
/// <param name="layerbreak3">Third layerbreak value, as applicable</param>
/// <returns>String representation of the media, including layer specification</returns>
/// TODO: Figure out why we have this and NormalizeDiscType as well
private static string? GetFixedMediaType(MediaType? mediaType, string? picIdentifier, long? size, long? layerbreak, long? layerbreak2, long? layerbreak3)
{
switch (mediaType)
{
case MediaType.DVD:
if (layerbreak != default && layerbreak != default(long))
return $"{mediaType.LongName()}-9";
else
return $"{mediaType.LongName()}-5";
case MediaType.BluRay:
if (layerbreak3 != default && layerbreak3 != default(long))
return $"{mediaType.LongName()}-128";
else if (layerbreak2 != default && layerbreak2 != default(long))
return $"{mediaType.LongName()}-100";
else if (layerbreak != default && layerbreak != default(long) && picIdentifier == SabreTools.Models.PIC.Constants.DiscTypeIdentifierROMUltra)
return $"{mediaType.LongName()}-66";
else if (layerbreak != default && layerbreak != default(long) && size > 53_687_063_712)
return $"{mediaType.LongName()}-66";
else if (layerbreak != default && layerbreak != default(long))
return $"{mediaType.LongName()}-50";
else if (picIdentifier == SabreTools.Models.PIC.Constants.DiscTypeIdentifierROMUltra)
return $"{mediaType.LongName()}-33";
else if (size > 26_843_531_856)
return $"{mediaType.LongName()}-33";
else
return $"{mediaType.LongName()}-25";
case MediaType.UMD:
if (layerbreak != default && layerbreak != default(long))
return $"{mediaType.LongName()}-DL";
else
return $"{mediaType.LongName()}-SL";
default:
return mediaType.LongName();
}
}
/// <summary>
/// Check if a site code is boolean or not
/// </summary>
/// <param name="siteCode">SiteCode to check</param>
/// <returns>True if the code field is a flag with no value, false otherwise</returns>
/// <remarks>TODO: This should move to Extensions at some point</remarks>
private static bool IsBoolean(SiteCode? siteCode)
{
return siteCode switch
{
SiteCode.PostgapType => true,
SiteCode.VCD => true,
_ => false,
};
}
/// <summary>
/// Order comment code tags according to Redump requirements
/// </summary>
/// <returns>Ordered list of KeyValuePairs representing the tags and values</returns>
private static List<KeyValuePair<SiteCode?, string>> OrderCommentTags(Dictionary<SiteCode, string> tags)
{
var sorted = new List<KeyValuePair<SiteCode?, string>>();
// If the input is invalid, just return an empty set
if (tags == null || tags.Count == 0)
return sorted;
// Identifying Info
if (tags.ContainsKey(SiteCode.AlternativeTitle))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.AlternativeTitle, tags[SiteCode.AlternativeTitle]));
if (tags.ContainsKey(SiteCode.AlternativeForeignTitle))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.AlternativeForeignTitle, tags[SiteCode.AlternativeForeignTitle]));
if (tags.ContainsKey(SiteCode.InternalName))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.InternalName, tags[SiteCode.InternalName]));
if (tags.ContainsKey(SiteCode.InternalSerialName))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.InternalSerialName, tags[SiteCode.InternalSerialName]));
if (tags.ContainsKey(SiteCode.VolumeLabel))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.VolumeLabel, tags[SiteCode.VolumeLabel]));
if (tags.ContainsKey(SiteCode.Multisession))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Multisession, tags[SiteCode.Multisession]));
if (tags.ContainsKey(SiteCode.UniversalHash))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.UniversalHash, tags[SiteCode.UniversalHash]));
if (tags.ContainsKey(SiteCode.RingNonZeroDataStart))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.RingNonZeroDataStart, tags[SiteCode.RingNonZeroDataStart]));
if (tags.ContainsKey(SiteCode.XMID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.XMID, tags[SiteCode.XMID]));
if (tags.ContainsKey(SiteCode.XeMID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.XeMID, tags[SiteCode.XeMID]));
if (tags.ContainsKey(SiteCode.DMIHash))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.DMIHash, tags[SiteCode.DMIHash]));
if (tags.ContainsKey(SiteCode.PFIHash))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PFIHash, tags[SiteCode.PFIHash]));
if (tags.ContainsKey(SiteCode.SSHash))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SSHash, tags[SiteCode.SSHash]));
if (tags.ContainsKey(SiteCode.SSVersion))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SSVersion, tags[SiteCode.SSVersion]));
if (tags.ContainsKey(SiteCode.Filename))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Filename, tags[SiteCode.Filename]));
if (tags.ContainsKey(SiteCode.BBFCRegistrationNumber))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.BBFCRegistrationNumber, tags[SiteCode.BBFCRegistrationNumber]));
if (tags.ContainsKey(SiteCode.CDProjektID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.CDProjektID, tags[SiteCode.CDProjektID]));
if (tags.ContainsKey(SiteCode.DiscHologramID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.DiscHologramID, tags[SiteCode.DiscHologramID]));
if (tags.ContainsKey(SiteCode.DNASDiscID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.DNASDiscID, tags[SiteCode.DNASDiscID]));
if (tags.ContainsKey(SiteCode.ISBN))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ISBN, tags[SiteCode.ISBN]));
if (tags.ContainsKey(SiteCode.ISSN))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ISSN, tags[SiteCode.ISSN]));
if (tags.ContainsKey(SiteCode.PPN))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PPN, tags[SiteCode.PPN]));
if (tags.ContainsKey(SiteCode.VFCCode))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.VFCCode, tags[SiteCode.VFCCode]));
if (tags.ContainsKey(SiteCode.Genre))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Genre, tags[SiteCode.Genre]));
if (tags.ContainsKey(SiteCode.Series))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Series, tags[SiteCode.Series]));
if (tags.ContainsKey(SiteCode.PostgapType))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PostgapType, tags[SiteCode.PostgapType]));
if (tags.ContainsKey(SiteCode.VCD))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.VCD, tags[SiteCode.VCD]));
// Publisher / Company IDs
if (tags.ContainsKey(SiteCode.AcclaimID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.AcclaimID, tags[SiteCode.AcclaimID]));
if (tags.ContainsKey(SiteCode.ActivisionID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ActivisionID, tags[SiteCode.ActivisionID]));
if (tags.ContainsKey(SiteCode.BandaiID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.BandaiID, tags[SiteCode.BandaiID]));
if (tags.ContainsKey(SiteCode.ElectronicArtsID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ElectronicArtsID, tags[SiteCode.ElectronicArtsID]));
if (tags.ContainsKey(SiteCode.FoxInteractiveID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.FoxInteractiveID, tags[SiteCode.FoxInteractiveID]));
if (tags.ContainsKey(SiteCode.GTInteractiveID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.GTInteractiveID, tags[SiteCode.GTInteractiveID]));
if (tags.ContainsKey(SiteCode.JASRACID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.JASRACID, tags[SiteCode.JASRACID]));
if (tags.ContainsKey(SiteCode.KingRecordsID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.KingRecordsID, tags[SiteCode.KingRecordsID]));
if (tags.ContainsKey(SiteCode.KoeiID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.KoeiID, tags[SiteCode.KoeiID]));
if (tags.ContainsKey(SiteCode.KonamiID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.KonamiID, tags[SiteCode.KonamiID]));
if (tags.ContainsKey(SiteCode.LucasArtsID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.LucasArtsID, tags[SiteCode.LucasArtsID]));
if (tags.ContainsKey(SiteCode.MicrosoftID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.MicrosoftID, tags[SiteCode.MicrosoftID]));
if (tags.ContainsKey(SiteCode.NaganoID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.NaganoID, tags[SiteCode.NaganoID]));
if (tags.ContainsKey(SiteCode.NamcoID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.NamcoID, tags[SiteCode.NamcoID]));
if (tags.ContainsKey(SiteCode.NipponIchiSoftwareID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.NipponIchiSoftwareID, tags[SiteCode.NipponIchiSoftwareID]));
if (tags.ContainsKey(SiteCode.OriginID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.OriginID, tags[SiteCode.OriginID]));
if (tags.ContainsKey(SiteCode.PonyCanyonID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PonyCanyonID, tags[SiteCode.PonyCanyonID]));
if (tags.ContainsKey(SiteCode.SegaID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SegaID, tags[SiteCode.SegaID]));
if (tags.ContainsKey(SiteCode.SelenID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SelenID, tags[SiteCode.SelenID]));
if (tags.ContainsKey(SiteCode.SierraID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SierraID, tags[SiteCode.SierraID]));
if (tags.ContainsKey(SiteCode.TaitoID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.TaitoID, tags[SiteCode.TaitoID]));
if (tags.ContainsKey(SiteCode.UbisoftID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.UbisoftID, tags[SiteCode.UbisoftID]));
if (tags.ContainsKey(SiteCode.ValveID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ValveID, tags[SiteCode.ValveID]));
return sorted;
}
/// <summary>
/// Order content code tags according to Redump requirements
/// </summary>
/// <returns>Ordered list of KeyValuePairs representing the tags and values</returns>
private static List<KeyValuePair<SiteCode?, string>> OrderContentTags(Dictionary<SiteCode, string> tags)
{
var sorted = new List<KeyValuePair<SiteCode?, string>>();
// If the input is invalid, just return an empty set
if (tags == null || tags.Count == 0)
return sorted;
// Games
if (tags.ContainsKey(SiteCode.Games))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Games, tags[SiteCode.Games]));
if (tags.ContainsKey(SiteCode.NetYarozeGames))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.NetYarozeGames, tags[SiteCode.NetYarozeGames]));
// Demos
if (tags.ContainsKey(SiteCode.PlayableDemos))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PlayableDemos, tags[SiteCode.PlayableDemos]));
if (tags.ContainsKey(SiteCode.RollingDemos))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.RollingDemos, tags[SiteCode.RollingDemos]));
if (tags.ContainsKey(SiteCode.TechDemos))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.TechDemos, tags[SiteCode.TechDemos]));
// Video
if (tags.ContainsKey(SiteCode.GameFootage))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.GameFootage, tags[SiteCode.GameFootage]));
if (tags.ContainsKey(SiteCode.Videos))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Videos, tags[SiteCode.Videos]));
// Miscellaneous
if (tags.ContainsKey(SiteCode.Patches))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Patches, tags[SiteCode.Patches]));
if (tags.ContainsKey(SiteCode.Savegames))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Savegames, tags[SiteCode.Savegames]));
if (tags.ContainsKey(SiteCode.Extras))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Extras, tags[SiteCode.Extras]));
return sorted;
}
#endregion
}
}

461
OldDotNet.cs Normal file
View File

@@ -0,0 +1,461 @@
#if NET20 || NET35
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace SabreTools.RedumpLib
{
/// <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Net/WebUtility.cs"/>
internal static class WebUtility
{
// some consts copied from Char / CharUnicodeInfo since we don't have friend access to those types
private const char HIGH_SURROGATE_START = '\uD800';
private const char LOW_SURROGATE_START = '\uDC00';
private const char LOW_SURROGATE_END = '\uDFFF';
private const int UNICODE_PLANE00_END = 0x00FFFF;
private const int UNICODE_PLANE01_START = 0x10000;
private const int UNICODE_PLANE16_END = 0x10FFFF;
public static string? HtmlDecode(string? value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
char[] valueSpan = value!.ToCharArray();
int index = Array.IndexOf(valueSpan, '&');
if (index < 0)
{
return value;
}
// In the worst case the decoded string has the same length.
// For small inputs we use stack allocation.
StringBuilder sb = value.Length <= 256 ?
new StringBuilder(256) :
new StringBuilder(value.Length);
sb.Append(valueSpan.Take(index).ToArray());
HtmlDecode(valueSpan.Skip(index).ToArray(), ref sb);
return sb.ToString();
}
private static void HtmlDecode(char[] input, ref StringBuilder output)
{
for (int i = 0; i < input.Length; i++)
{
char ch = input[i];
if (ch == '&')
{
// We found a '&'. Now look for the next ';' or '&'. The idea is that
// if we find another '&' before finding a ';', then this is not an entity,
// and the next '&' might start a real entity (VSWhidbey 275184)
char[] inputSlice = input.Skip(i + 1).ToArray();
int semicolonPos = Array.IndexOf(inputSlice, ';');
int ampersandPos = Array.IndexOf(inputSlice, '&');
int entityLength;
if (semicolonPos > -1 && ampersandPos > -1)
entityLength = Math.Min(semicolonPos, ampersandPos);
else if (semicolonPos <= -1 && ampersandPos > -1)
entityLength = ampersandPos;
else if (semicolonPos > -1 && ampersandPos <= -1)
entityLength = semicolonPos;
else
entityLength = -1;
if (entityLength >= 0 && inputSlice[entityLength] == ';')
{
int entityEndPosition = (i + 1) + entityLength;
if (entityLength > 1 && inputSlice[0] == '#')
{
// The # syntax can be in decimal or hex, e.g.
// &#229; --> decimal
// &#xE5; --> same char in hex
// See http://www.w3.org/TR/REC-html40/charset.html#entities
bool parsedSuccessfully = inputSlice[1] == 'x' || inputSlice[1] == 'X'
? uint.TryParse(new string(inputSlice.Skip(2).Take(entityLength - 2).ToArray()), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out uint parsedValue)
: uint.TryParse(new string(inputSlice.Skip(1).Take(entityLength - 1).ToArray()), NumberStyles.Integer, CultureInfo.InvariantCulture, out parsedValue);
if (parsedSuccessfully)
{
// decoded character must be U+0000 .. U+10FFFF, excluding surrogates
parsedSuccessfully = ((parsedValue < HIGH_SURROGATE_START) || (LOW_SURROGATE_END < parsedValue && parsedValue <= UNICODE_PLANE16_END));
}
if (parsedSuccessfully)
{
if (parsedValue <= UNICODE_PLANE00_END)
{
// single character
output.Append((char)parsedValue);
}
else
{
// multi-character
ConvertSmpToUtf16(parsedValue, out char leadingSurrogate, out char trailingSurrogate);
output.Append(leadingSurrogate);
output.Append(trailingSurrogate);
}
i = entityEndPosition; // already looked at everything until semicolon
continue;
}
}
else
{
char[] entity = inputSlice.Take(entityLength).ToArray();
i = entityEndPosition; // already looked at everything until semicolon
char entityChar = HtmlEntities.Lookup(entity);
if (entityChar != (char)0)
{
ch = entityChar;
}
else
{
output.Append('&');
output.Append(entity);
output.Append(';');
continue;
}
}
}
}
output.Append(ch);
}
}
// similar to Char.ConvertFromUtf32, but doesn't check arguments or generate strings
// input is assumed to be an SMP character
private static void ConvertSmpToUtf16(uint smpChar, out char leadingSurrogate, out char trailingSurrogate)
{
int utf32 = (int)(smpChar - UNICODE_PLANE01_START);
leadingSurrogate = (char)((utf32 / 0x400) + HIGH_SURROGATE_START);
trailingSurrogate = (char)((utf32 % 0x400) + LOW_SURROGATE_START);
}
// helper class for lookup of HTML encoding entities
private static class HtmlEntities
{
// The list is from http://www.w3.org/TR/REC-html40/sgml/entities.html, except for &apos;, which
// is defined in http://www.w3.org/TR/2008/REC-xml-20081126/#sec-predefined-ent.
private static Dictionary<ulong, char> InitializeLookupTable()
{
byte[] tableData =
[
0x74, 0x6F, 0x75, 0x71, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("quot")*/ 0x22, 0x00, /*'\x0022'*/
0x70, 0x6D, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("amp")*/ 0x26, 0x00, /*'\x0026'*/
0x73, 0x6F, 0x70, 0x61, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("apos")*/ 0x27, 0x00, /*'\x0027'*/
0x74, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("lt")*/ 0x3C, 0x00, /*'\x003c'*/
0x74, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("gt")*/ 0x3E, 0x00, /*'\x003e'*/
0x70, 0x73, 0x62, 0x6E, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("nbsp")*/ 0xA0, 0x00, /*'\x00a0'*/
0x6C, 0x63, 0x78, 0x65, 0x69, 0x00, 0x00, 0x00, /*ToUInt64Key("iexcl")*/ 0xA1, 0x00, /*'\x00a1'*/
0x74, 0x6E, 0x65, 0x63, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("cent")*/ 0xA2, 0x00, /*'\x00a2'*/
0x64, 0x6E, 0x75, 0x6F, 0x70, 0x00, 0x00, 0x00, /*ToUInt64Key("pound")*/ 0xA3, 0x00, /*'\x00a3'*/
0x6E, 0x65, 0x72, 0x72, 0x75, 0x63, 0x00, 0x00, /*ToUInt64Key("curren")*/ 0xA4, 0x00, /*'\x00a4'*/
0x6E, 0x65, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("yen")*/ 0xA5, 0x00, /*'\x00a5'*/
0x72, 0x61, 0x62, 0x76, 0x72, 0x62, 0x00, 0x00, /*ToUInt64Key("brvbar")*/ 0xA6, 0x00, /*'\x00a6'*/
0x74, 0x63, 0x65, 0x73, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sect")*/ 0xA7, 0x00, /*'\x00a7'*/
0x6C, 0x6D, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("uml")*/ 0xA8, 0x00, /*'\x00a8'*/
0x79, 0x70, 0x6F, 0x63, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("copy")*/ 0xA9, 0x00, /*'\x00a9'*/
0x66, 0x64, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("ordf")*/ 0xAA, 0x00, /*'\x00aa'*/
0x6F, 0x75, 0x71, 0x61, 0x6C, 0x00, 0x00, 0x00, /*ToUInt64Key("laquo")*/ 0xAB, 0x00, /*'\x00ab'*/
0x74, 0x6F, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("not")*/ 0xAC, 0x00, /*'\x00ac'*/
0x79, 0x68, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("shy")*/ 0xAD, 0x00, /*'\x00ad'*/
0x67, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("reg")*/ 0xAE, 0x00, /*'\x00ae'*/
0x72, 0x63, 0x61, 0x6D, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("macr")*/ 0xAF, 0x00, /*'\x00af'*/
0x67, 0x65, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("deg")*/ 0xB0, 0x00, /*'\x00b0'*/
0x6E, 0x6D, 0x73, 0x75, 0x6C, 0x70, 0x00, 0x00, /*ToUInt64Key("plusmn")*/ 0xB1, 0x00, /*'\x00b1'*/
0x32, 0x70, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sup2")*/ 0xB2, 0x00, /*'\x00b2'*/
0x33, 0x70, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sup3")*/ 0xB3, 0x00, /*'\x00b3'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x00, 0x00, 0x00, /*ToUInt64Key("acute")*/ 0xB4, 0x00, /*'\x00b4'*/
0x6F, 0x72, 0x63, 0x69, 0x6D, 0x00, 0x00, 0x00, /*ToUInt64Key("micro")*/ 0xB5, 0x00, /*'\x00b5'*/
0x61, 0x72, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("para")*/ 0xB6, 0x00, /*'\x00b6'*/
0x74, 0x6F, 0x64, 0x64, 0x69, 0x6D, 0x00, 0x00, /*ToUInt64Key("middot")*/ 0xB7, 0x00, /*'\x00b7'*/
0x6C, 0x69, 0x64, 0x65, 0x63, 0x00, 0x00, 0x00, /*ToUInt64Key("cedil")*/ 0xB8, 0x00, /*'\x00b8'*/
0x31, 0x70, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sup1")*/ 0xB9, 0x00, /*'\x00b9'*/
0x6D, 0x64, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("ordm")*/ 0xBA, 0x00, /*'\x00ba'*/
0x6F, 0x75, 0x71, 0x61, 0x72, 0x00, 0x00, 0x00, /*ToUInt64Key("raquo")*/ 0xBB, 0x00, /*'\x00bb'*/
0x34, 0x31, 0x63, 0x61, 0x72, 0x66, 0x00, 0x00, /*ToUInt64Key("frac14")*/ 0xBC, 0x00, /*'\x00bc'*/
0x32, 0x31, 0x63, 0x61, 0x72, 0x66, 0x00, 0x00, /*ToUInt64Key("frac12")*/ 0xBD, 0x00, /*'\x00bd'*/
0x34, 0x33, 0x63, 0x61, 0x72, 0x66, 0x00, 0x00, /*ToUInt64Key("frac34")*/ 0xBE, 0x00, /*'\x00be'*/
0x74, 0x73, 0x65, 0x75, 0x71, 0x69, 0x00, 0x00, /*ToUInt64Key("iquest")*/ 0xBF, 0x00, /*'\x00bf'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x41, 0x00, 0x00, /*ToUInt64Key("Agrave")*/ 0xC0, 0x00, /*'\x00c0'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x41, 0x00, 0x00, /*ToUInt64Key("Aacute")*/ 0xC1, 0x00, /*'\x00c1'*/
0x63, 0x72, 0x69, 0x63, 0x41, 0x00, 0x00, 0x00, /*ToUInt64Key("Acirc")*/ 0xC2, 0x00, /*'\x00c2'*/
0x65, 0x64, 0x6C, 0x69, 0x74, 0x41, 0x00, 0x00, /*ToUInt64Key("Atilde")*/ 0xC3, 0x00, /*'\x00c3'*/
0x6C, 0x6D, 0x75, 0x41, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Auml")*/ 0xC4, 0x00, /*'\x00c4'*/
0x67, 0x6E, 0x69, 0x72, 0x41, 0x00, 0x00, 0x00, /*ToUInt64Key("Aring")*/ 0xC5, 0x00, /*'\x00c5'*/
0x67, 0x69, 0x6C, 0x45, 0x41, 0x00, 0x00, 0x00, /*ToUInt64Key("AElig")*/ 0xC6, 0x00, /*'\x00c6'*/
0x6C, 0x69, 0x64, 0x65, 0x63, 0x43, 0x00, 0x00, /*ToUInt64Key("Ccedil")*/ 0xC7, 0x00, /*'\x00c7'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x45, 0x00, 0x00, /*ToUInt64Key("Egrave")*/ 0xC8, 0x00, /*'\x00c8'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x45, 0x00, 0x00, /*ToUInt64Key("Eacute")*/ 0xC9, 0x00, /*'\x00c9'*/
0x63, 0x72, 0x69, 0x63, 0x45, 0x00, 0x00, 0x00, /*ToUInt64Key("Ecirc")*/ 0xCA, 0x00, /*'\x00ca'*/
0x6C, 0x6D, 0x75, 0x45, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Euml")*/ 0xCB, 0x00, /*'\x00cb'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x49, 0x00, 0x00, /*ToUInt64Key("Igrave")*/ 0xCC, 0x00, /*'\x00cc'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x49, 0x00, 0x00, /*ToUInt64Key("Iacute")*/ 0xCD, 0x00, /*'\x00cd'*/
0x63, 0x72, 0x69, 0x63, 0x49, 0x00, 0x00, 0x00, /*ToUInt64Key("Icirc")*/ 0xCE, 0x00, /*'\x00ce'*/
0x6C, 0x6D, 0x75, 0x49, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Iuml")*/ 0xCF, 0x00, /*'\x00cf'*/
0x48, 0x54, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("ETH")*/ 0xD0, 0x00, /*'\x00d0'*/
0x65, 0x64, 0x6C, 0x69, 0x74, 0x4E, 0x00, 0x00, /*ToUInt64Key("Ntilde")*/ 0xD1, 0x00, /*'\x00d1'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x4F, 0x00, 0x00, /*ToUInt64Key("Ograve")*/ 0xD2, 0x00, /*'\x00d2'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x4F, 0x00, 0x00, /*ToUInt64Key("Oacute")*/ 0xD3, 0x00, /*'\x00d3'*/
0x63, 0x72, 0x69, 0x63, 0x4F, 0x00, 0x00, 0x00, /*ToUInt64Key("Ocirc")*/ 0xD4, 0x00, /*'\x00d4'*/
0x65, 0x64, 0x6C, 0x69, 0x74, 0x4F, 0x00, 0x00, /*ToUInt64Key("Otilde")*/ 0xD5, 0x00, /*'\x00d5'*/
0x6C, 0x6D, 0x75, 0x4F, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Ouml")*/ 0xD6, 0x00, /*'\x00d6'*/
0x73, 0x65, 0x6D, 0x69, 0x74, 0x00, 0x00, 0x00, /*ToUInt64Key("times")*/ 0xD7, 0x00, /*'\x00d7'*/
0x68, 0x73, 0x61, 0x6C, 0x73, 0x4F, 0x00, 0x00, /*ToUInt64Key("Oslash")*/ 0xD8, 0x00, /*'\x00d8'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x55, 0x00, 0x00, /*ToUInt64Key("Ugrave")*/ 0xD9, 0x00, /*'\x00d9'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x55, 0x00, 0x00, /*ToUInt64Key("Uacute")*/ 0xDA, 0x00, /*'\x00da'*/
0x63, 0x72, 0x69, 0x63, 0x55, 0x00, 0x00, 0x00, /*ToUInt64Key("Ucirc")*/ 0xDB, 0x00, /*'\x00db'*/
0x6C, 0x6D, 0x75, 0x55, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Uuml")*/ 0xDC, 0x00, /*'\x00dc'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x59, 0x00, 0x00, /*ToUInt64Key("Yacute")*/ 0xDD, 0x00, /*'\x00dd'*/
0x4E, 0x52, 0x4F, 0x48, 0x54, 0x00, 0x00, 0x00, /*ToUInt64Key("THORN")*/ 0xDE, 0x00, /*'\x00de'*/
0x67, 0x69, 0x6C, 0x7A, 0x73, 0x00, 0x00, 0x00, /*ToUInt64Key("szlig")*/ 0xDF, 0x00, /*'\x00df'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x61, 0x00, 0x00, /*ToUInt64Key("agrave")*/ 0xE0, 0x00, /*'\x00e0'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x61, 0x00, 0x00, /*ToUInt64Key("aacute")*/ 0xE1, 0x00, /*'\x00e1'*/
0x63, 0x72, 0x69, 0x63, 0x61, 0x00, 0x00, 0x00, /*ToUInt64Key("acirc")*/ 0xE2, 0x00, /*'\x00e2'*/
0x65, 0x64, 0x6C, 0x69, 0x74, 0x61, 0x00, 0x00, /*ToUInt64Key("atilde")*/ 0xE3, 0x00, /*'\x00e3'*/
0x6C, 0x6D, 0x75, 0x61, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("auml")*/ 0xE4, 0x00, /*'\x00e4'*/
0x67, 0x6E, 0x69, 0x72, 0x61, 0x00, 0x00, 0x00, /*ToUInt64Key("aring")*/ 0xE5, 0x00, /*'\x00e5'*/
0x67, 0x69, 0x6C, 0x65, 0x61, 0x00, 0x00, 0x00, /*ToUInt64Key("aelig")*/ 0xE6, 0x00, /*'\x00e6'*/
0x6C, 0x69, 0x64, 0x65, 0x63, 0x63, 0x00, 0x00, /*ToUInt64Key("ccedil")*/ 0xE7, 0x00, /*'\x00e7'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x65, 0x00, 0x00, /*ToUInt64Key("egrave")*/ 0xE8, 0x00, /*'\x00e8'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x65, 0x00, 0x00, /*ToUInt64Key("eacute")*/ 0xE9, 0x00, /*'\x00e9'*/
0x63, 0x72, 0x69, 0x63, 0x65, 0x00, 0x00, 0x00, /*ToUInt64Key("ecirc")*/ 0xEA, 0x00, /*'\x00ea'*/
0x6C, 0x6D, 0x75, 0x65, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("euml")*/ 0xEB, 0x00, /*'\x00eb'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x69, 0x00, 0x00, /*ToUInt64Key("igrave")*/ 0xEC, 0x00, /*'\x00ec'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x69, 0x00, 0x00, /*ToUInt64Key("iacute")*/ 0xED, 0x00, /*'\x00ed'*/
0x63, 0x72, 0x69, 0x63, 0x69, 0x00, 0x00, 0x00, /*ToUInt64Key("icirc")*/ 0xEE, 0x00, /*'\x00ee'*/
0x6C, 0x6D, 0x75, 0x69, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("iuml")*/ 0xEF, 0x00, /*'\x00ef'*/
0x68, 0x74, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("eth")*/ 0xF0, 0x00, /*'\x00f0'*/
0x65, 0x64, 0x6C, 0x69, 0x74, 0x6E, 0x00, 0x00, /*ToUInt64Key("ntilde")*/ 0xF1, 0x00, /*'\x00f1'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x6F, 0x00, 0x00, /*ToUInt64Key("ograve")*/ 0xF2, 0x00, /*'\x00f2'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x6F, 0x00, 0x00, /*ToUInt64Key("oacute")*/ 0xF3, 0x00, /*'\x00f3'*/
0x63, 0x72, 0x69, 0x63, 0x6F, 0x00, 0x00, 0x00, /*ToUInt64Key("ocirc")*/ 0xF4, 0x00, /*'\x00f4'*/
0x65, 0x64, 0x6C, 0x69, 0x74, 0x6F, 0x00, 0x00, /*ToUInt64Key("otilde")*/ 0xF5, 0x00, /*'\x00f5'*/
0x6C, 0x6D, 0x75, 0x6F, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("ouml")*/ 0xF6, 0x00, /*'\x00f6'*/
0x65, 0x64, 0x69, 0x76, 0x69, 0x64, 0x00, 0x00, /*ToUInt64Key("divide")*/ 0xF7, 0x00, /*'\x00f7'*/
0x68, 0x73, 0x61, 0x6C, 0x73, 0x6F, 0x00, 0x00, /*ToUInt64Key("oslash")*/ 0xF8, 0x00, /*'\x00f8'*/
0x65, 0x76, 0x61, 0x72, 0x67, 0x75, 0x00, 0x00, /*ToUInt64Key("ugrave")*/ 0xF9, 0x00, /*'\x00f9'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x75, 0x00, 0x00, /*ToUInt64Key("uacute")*/ 0xFA, 0x00, /*'\x00fa'*/
0x63, 0x72, 0x69, 0x63, 0x75, 0x00, 0x00, 0x00, /*ToUInt64Key("ucirc")*/ 0xFB, 0x00, /*'\x00fb'*/
0x6C, 0x6D, 0x75, 0x75, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("uuml")*/ 0xFC, 0x00, /*'\x00fc'*/
0x65, 0x74, 0x75, 0x63, 0x61, 0x79, 0x00, 0x00, /*ToUInt64Key("yacute")*/ 0xFD, 0x00, /*'\x00fd'*/
0x6E, 0x72, 0x6F, 0x68, 0x74, 0x00, 0x00, 0x00, /*ToUInt64Key("thorn")*/ 0xFE, 0x00, /*'\x00fe'*/
0x6C, 0x6D, 0x75, 0x79, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("yuml")*/ 0xFF, 0x00, /*'\x00ff'*/
0x67, 0x69, 0x6C, 0x45, 0x4F, 0x00, 0x00, 0x00, /*ToUInt64Key("OElig")*/ 0x52, 0x01, /*'\x0152'*/
0x67, 0x69, 0x6C, 0x65, 0x6F, 0x00, 0x00, 0x00, /*ToUInt64Key("oelig")*/ 0x53, 0x01, /*'\x0153'*/
0x6E, 0x6F, 0x72, 0x61, 0x63, 0x53, 0x00, 0x00, /*ToUInt64Key("Scaron")*/ 0x60, 0x01, /*'\x0160'*/
0x6E, 0x6F, 0x72, 0x61, 0x63, 0x73, 0x00, 0x00, /*ToUInt64Key("scaron")*/ 0x61, 0x01, /*'\x0161'*/
0x6C, 0x6D, 0x75, 0x59, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Yuml")*/ 0x78, 0x01, /*'\x0178'*/
0x66, 0x6F, 0x6E, 0x66, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("fnof")*/ 0x92, 0x01, /*'\x0192'*/
0x63, 0x72, 0x69, 0x63, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("circ")*/ 0xC6, 0x02, /*'\x02c6'*/
0x65, 0x64, 0x6C, 0x69, 0x74, 0x00, 0x00, 0x00, /*ToUInt64Key("tilde")*/ 0xDC, 0x02, /*'\x02dc'*/
0x61, 0x68, 0x70, 0x6C, 0x41, 0x00, 0x00, 0x00, /*ToUInt64Key("Alpha")*/ 0x91, 0x03, /*'\x0391'*/
0x61, 0x74, 0x65, 0x42, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Beta")*/ 0x92, 0x03, /*'\x0392'*/
0x61, 0x6D, 0x6D, 0x61, 0x47, 0x00, 0x00, 0x00, /*ToUInt64Key("Gamma")*/ 0x93, 0x03, /*'\x0393'*/
0x61, 0x74, 0x6C, 0x65, 0x44, 0x00, 0x00, 0x00, /*ToUInt64Key("Delta")*/ 0x94, 0x03, /*'\x0394'*/
0x6E, 0x6F, 0x6C, 0x69, 0x73, 0x70, 0x45, 0x00, /*ToUInt64Key("Epsilon")*/ 0x95, 0x03, /*'\x0395'*/
0x61, 0x74, 0x65, 0x5A, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Zeta")*/ 0x96, 0x03, /*'\x0396'*/
0x61, 0x74, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Eta")*/ 0x97, 0x03, /*'\x0397'*/
0x61, 0x74, 0x65, 0x68, 0x54, 0x00, 0x00, 0x00, /*ToUInt64Key("Theta")*/ 0x98, 0x03, /*'\x0398'*/
0x61, 0x74, 0x6F, 0x49, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Iota")*/ 0x99, 0x03, /*'\x0399'*/
0x61, 0x70, 0x70, 0x61, 0x4B, 0x00, 0x00, 0x00, /*ToUInt64Key("Kappa")*/ 0x9A, 0x03, /*'\x039a'*/
0x61, 0x64, 0x62, 0x6D, 0x61, 0x4C, 0x00, 0x00, /*ToUInt64Key("Lambda")*/ 0x9B, 0x03, /*'\x039b'*/
0x75, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Mu")*/ 0x9C, 0x03, /*'\x039c'*/
0x75, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Nu")*/ 0x9D, 0x03, /*'\x039d'*/
0x69, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Xi")*/ 0x9E, 0x03, /*'\x039e'*/
0x6E, 0x6F, 0x72, 0x63, 0x69, 0x6D, 0x4F, 0x00, /*ToUInt64Key("Omicron")*/ 0x9F, 0x03, /*'\x039f'*/
0x69, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Pi")*/ 0xA0, 0x03, /*'\x03a0'*/
0x6F, 0x68, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Rho")*/ 0xA1, 0x03, /*'\x03a1'*/
0x61, 0x6D, 0x67, 0x69, 0x53, 0x00, 0x00, 0x00, /*ToUInt64Key("Sigma")*/ 0xA3, 0x03, /*'\x03a3'*/
0x75, 0x61, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Tau")*/ 0xA4, 0x03, /*'\x03a4'*/
0x6E, 0x6F, 0x6C, 0x69, 0x73, 0x70, 0x55, 0x00, /*ToUInt64Key("Upsilon")*/ 0xA5, 0x03, /*'\x03a5'*/
0x69, 0x68, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Phi")*/ 0xA6, 0x03, /*'\x03a6'*/
0x69, 0x68, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Chi")*/ 0xA7, 0x03, /*'\x03a7'*/
0x69, 0x73, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("Psi")*/ 0xA8, 0x03, /*'\x03a8'*/
0x61, 0x67, 0x65, 0x6D, 0x4F, 0x00, 0x00, 0x00, /*ToUInt64Key("Omega")*/ 0xA9, 0x03, /*'\x03a9'*/
0x61, 0x68, 0x70, 0x6C, 0x61, 0x00, 0x00, 0x00, /*ToUInt64Key("alpha")*/ 0xB1, 0x03, /*'\x03b1'*/
0x61, 0x74, 0x65, 0x62, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("beta")*/ 0xB2, 0x03, /*'\x03b2'*/
0x61, 0x6D, 0x6D, 0x61, 0x67, 0x00, 0x00, 0x00, /*ToUInt64Key("gamma")*/ 0xB3, 0x03, /*'\x03b3'*/
0x61, 0x74, 0x6C, 0x65, 0x64, 0x00, 0x00, 0x00, /*ToUInt64Key("delta")*/ 0xB4, 0x03, /*'\x03b4'*/
0x6E, 0x6F, 0x6C, 0x69, 0x73, 0x70, 0x65, 0x00, /*ToUInt64Key("epsilon")*/ 0xB5, 0x03, /*'\x03b5'*/
0x61, 0x74, 0x65, 0x7A, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("zeta")*/ 0xB6, 0x03, /*'\x03b6'*/
0x61, 0x74, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("eta")*/ 0xB7, 0x03, /*'\x03b7'*/
0x61, 0x74, 0x65, 0x68, 0x74, 0x00, 0x00, 0x00, /*ToUInt64Key("theta")*/ 0xB8, 0x03, /*'\x03b8'*/
0x61, 0x74, 0x6F, 0x69, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("iota")*/ 0xB9, 0x03, /*'\x03b9'*/
0x61, 0x70, 0x70, 0x61, 0x6B, 0x00, 0x00, 0x00, /*ToUInt64Key("kappa")*/ 0xBA, 0x03, /*'\x03ba'*/
0x61, 0x64, 0x62, 0x6D, 0x61, 0x6C, 0x00, 0x00, /*ToUInt64Key("lambda")*/ 0xBB, 0x03, /*'\x03bb'*/
0x75, 0x6D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("mu")*/ 0xBC, 0x03, /*'\x03bc'*/
0x75, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("nu")*/ 0xBD, 0x03, /*'\x03bd'*/
0x69, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("xi")*/ 0xBE, 0x03, /*'\x03be'*/
0x6E, 0x6F, 0x72, 0x63, 0x69, 0x6D, 0x6F, 0x00, /*ToUInt64Key("omicron")*/ 0xBF, 0x03, /*'\x03bf'*/
0x69, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("pi")*/ 0xC0, 0x03, /*'\x03c0'*/
0x6F, 0x68, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("rho")*/ 0xC1, 0x03, /*'\x03c1'*/
0x66, 0x61, 0x6D, 0x67, 0x69, 0x73, 0x00, 0x00, /*ToUInt64Key("sigmaf")*/ 0xC2, 0x03, /*'\x03c2'*/
0x61, 0x6D, 0x67, 0x69, 0x73, 0x00, 0x00, 0x00, /*ToUInt64Key("sigma")*/ 0xC3, 0x03, /*'\x03c3'*/
0x75, 0x61, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("tau")*/ 0xC4, 0x03, /*'\x03c4'*/
0x6E, 0x6F, 0x6C, 0x69, 0x73, 0x70, 0x75, 0x00, /*ToUInt64Key("upsilon")*/ 0xC5, 0x03, /*'\x03c5'*/
0x69, 0x68, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("phi")*/ 0xC6, 0x03, /*'\x03c6'*/
0x69, 0x68, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("chi")*/ 0xC7, 0x03, /*'\x03c7'*/
0x69, 0x73, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("psi")*/ 0xC8, 0x03, /*'\x03c8'*/
0x61, 0x67, 0x65, 0x6D, 0x6F, 0x00, 0x00, 0x00, /*ToUInt64Key("omega")*/ 0xC9, 0x03, /*'\x03c9'*/
0x6D, 0x79, 0x73, 0x61, 0x74, 0x65, 0x68, 0x74, /*ToUInt64Key("thetasym")*/0xD1, 0x03, /*'\x03d1'*/
0x68, 0x69, 0x73, 0x70, 0x75, 0x00, 0x00, 0x00, /*ToUInt64Key("upsih")*/ 0xD2, 0x03, /*'\x03d2'*/
0x76, 0x69, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("piv")*/ 0xD6, 0x03, /*'\x03d6'*/
0x70, 0x73, 0x6E, 0x65, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("ensp")*/ 0x02, 0x20, /*'\x2002'*/
0x70, 0x73, 0x6D, 0x65, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("emsp")*/ 0x03, 0x20, /*'\x2003'*/
0x70, 0x73, 0x6E, 0x69, 0x68, 0x74, 0x00, 0x00, /*ToUInt64Key("thinsp")*/ 0x09, 0x20, /*'\x2009'*/
0x6A, 0x6E, 0x77, 0x7A, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("zwnj")*/ 0x0C, 0x20, /*'\x200c'*/
0x6A, 0x77, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("zwj")*/ 0x0D, 0x20, /*'\x200d'*/
0x6D, 0x72, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("lrm")*/ 0x0E, 0x20, /*'\x200e'*/
0x6D, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("rlm")*/ 0x0F, 0x20, /*'\x200f'*/
0x68, 0x73, 0x61, 0x64, 0x6E, 0x00, 0x00, 0x00, /*ToUInt64Key("ndash")*/ 0x13, 0x20, /*'\x2013'*/
0x68, 0x73, 0x61, 0x64, 0x6D, 0x00, 0x00, 0x00, /*ToUInt64Key("mdash")*/ 0x14, 0x20, /*'\x2014'*/
0x6F, 0x75, 0x71, 0x73, 0x6C, 0x00, 0x00, 0x00, /*ToUInt64Key("lsquo")*/ 0x18, 0x20, /*'\x2018'*/
0x6F, 0x75, 0x71, 0x73, 0x72, 0x00, 0x00, 0x00, /*ToUInt64Key("rsquo")*/ 0x19, 0x20, /*'\x2019'*/
0x6F, 0x75, 0x71, 0x62, 0x73, 0x00, 0x00, 0x00, /*ToUInt64Key("sbquo")*/ 0x1A, 0x20, /*'\x201a'*/
0x6F, 0x75, 0x71, 0x64, 0x6C, 0x00, 0x00, 0x00, /*ToUInt64Key("ldquo")*/ 0x1C, 0x20, /*'\x201c'*/
0x6F, 0x75, 0x71, 0x64, 0x72, 0x00, 0x00, 0x00, /*ToUInt64Key("rdquo")*/ 0x1D, 0x20, /*'\x201d'*/
0x6F, 0x75, 0x71, 0x64, 0x62, 0x00, 0x00, 0x00, /*ToUInt64Key("bdquo")*/ 0x1E, 0x20, /*'\x201e'*/
0x72, 0x65, 0x67, 0x67, 0x61, 0x64, 0x00, 0x00, /*ToUInt64Key("dagger")*/ 0x20, 0x20, /*'\x2020'*/
0x72, 0x65, 0x67, 0x67, 0x61, 0x44, 0x00, 0x00, /*ToUInt64Key("Dagger")*/ 0x21, 0x20, /*'\x2021'*/
0x6C, 0x6C, 0x75, 0x62, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("bull")*/ 0x22, 0x20, /*'\x2022'*/
0x70, 0x69, 0x6C, 0x6C, 0x65, 0x68, 0x00, 0x00, /*ToUInt64Key("hellip")*/ 0x26, 0x20, /*'\x2026'*/
0x6C, 0x69, 0x6D, 0x72, 0x65, 0x70, 0x00, 0x00, /*ToUInt64Key("permil")*/ 0x30, 0x20, /*'\x2030'*/
0x65, 0x6D, 0x69, 0x72, 0x70, 0x00, 0x00, 0x00, /*ToUInt64Key("prime")*/ 0x32, 0x20, /*'\x2032'*/
0x65, 0x6D, 0x69, 0x72, 0x50, 0x00, 0x00, 0x00, /*ToUInt64Key("Prime")*/ 0x33, 0x20, /*'\x2033'*/
0x6F, 0x75, 0x71, 0x61, 0x73, 0x6C, 0x00, 0x00, /*ToUInt64Key("lsaquo")*/ 0x39, 0x20, /*'\x2039'*/
0x6F, 0x75, 0x71, 0x61, 0x73, 0x72, 0x00, 0x00, /*ToUInt64Key("rsaquo")*/ 0x3A, 0x20, /*'\x203a'*/
0x65, 0x6E, 0x69, 0x6C, 0x6F, 0x00, 0x00, 0x00, /*ToUInt64Key("oline")*/ 0x3E, 0x20, /*'\x203e'*/
0x6C, 0x73, 0x61, 0x72, 0x66, 0x00, 0x00, 0x00, /*ToUInt64Key("frasl")*/ 0x44, 0x20, /*'\x2044'*/
0x6F, 0x72, 0x75, 0x65, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("euro")*/ 0xAC, 0x20, /*'\x20ac'*/
0x65, 0x67, 0x61, 0x6D, 0x69, 0x00, 0x00, 0x00, /*ToUInt64Key("image")*/ 0x11, 0x21, /*'\x2111'*/
0x70, 0x72, 0x65, 0x69, 0x65, 0x77, 0x00, 0x00, /*ToUInt64Key("weierp")*/ 0x18, 0x21, /*'\x2118'*/
0x6C, 0x61, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("real")*/ 0x1C, 0x21, /*'\x211c'*/
0x65, 0x64, 0x61, 0x72, 0x74, 0x00, 0x00, 0x00, /*ToUInt64Key("trade")*/ 0x22, 0x21, /*'\x2122'*/
0x6D, 0x79, 0x73, 0x66, 0x65, 0x6C, 0x61, 0x00, /*ToUInt64Key("alefsym")*/ 0x35, 0x21, /*'\x2135'*/
0x72, 0x72, 0x61, 0x6C, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("larr")*/ 0x90, 0x21, /*'\x2190'*/
0x72, 0x72, 0x61, 0x75, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("uarr")*/ 0x91, 0x21, /*'\x2191'*/
0x72, 0x72, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("rarr")*/ 0x92, 0x21, /*'\x2192'*/
0x72, 0x72, 0x61, 0x64, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("darr")*/ 0x93, 0x21, /*'\x2193'*/
0x72, 0x72, 0x61, 0x68, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("harr")*/ 0x94, 0x21, /*'\x2194'*/
0x72, 0x72, 0x61, 0x72, 0x63, 0x00, 0x00, 0x00, /*ToUInt64Key("crarr")*/ 0xB5, 0x21, /*'\x21b5'*/
0x72, 0x72, 0x41, 0x6C, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("lArr")*/ 0xD0, 0x21, /*'\x21d0'*/
0x72, 0x72, 0x41, 0x75, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("uArr")*/ 0xD1, 0x21, /*'\x21d1'*/
0x72, 0x72, 0x41, 0x72, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("rArr")*/ 0xD2, 0x21, /*'\x21d2'*/
0x72, 0x72, 0x41, 0x64, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("dArr")*/ 0xD3, 0x21, /*'\x21d3'*/
0x72, 0x72, 0x41, 0x68, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("hArr")*/ 0xD4, 0x21, /*'\x21d4'*/
0x6C, 0x6C, 0x61, 0x72, 0x6F, 0x66, 0x00, 0x00, /*ToUInt64Key("forall")*/ 0x00, 0x22, /*'\x2200'*/
0x74, 0x72, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("part")*/ 0x02, 0x22, /*'\x2202'*/
0x74, 0x73, 0x69, 0x78, 0x65, 0x00, 0x00, 0x00, /*ToUInt64Key("exist")*/ 0x03, 0x22, /*'\x2203'*/
0x79, 0x74, 0x70, 0x6D, 0x65, 0x00, 0x00, 0x00, /*ToUInt64Key("empty")*/ 0x05, 0x22, /*'\x2205'*/
0x61, 0x6C, 0x62, 0x61, 0x6E, 0x00, 0x00, 0x00, /*ToUInt64Key("nabla")*/ 0x07, 0x22, /*'\x2207'*/
0x6E, 0x69, 0x73, 0x69, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("isin")*/ 0x08, 0x22, /*'\x2208'*/
0x6E, 0x69, 0x74, 0x6F, 0x6E, 0x00, 0x00, 0x00, /*ToUInt64Key("notin")*/ 0x09, 0x22, /*'\x2209'*/
0x69, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("ni")*/ 0x0B, 0x22, /*'\x220b'*/
0x64, 0x6F, 0x72, 0x70, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("prod")*/ 0x0F, 0x22, /*'\x220f'*/
0x6D, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sum")*/ 0x11, 0x22, /*'\x2211'*/
0x73, 0x75, 0x6E, 0x69, 0x6D, 0x00, 0x00, 0x00, /*ToUInt64Key("minus")*/ 0x12, 0x22, /*'\x2212'*/
0x74, 0x73, 0x61, 0x77, 0x6F, 0x6C, 0x00, 0x00, /*ToUInt64Key("lowast")*/ 0x17, 0x22, /*'\x2217'*/
0x63, 0x69, 0x64, 0x61, 0x72, 0x00, 0x00, 0x00, /*ToUInt64Key("radic")*/ 0x1A, 0x22, /*'\x221a'*/
0x70, 0x6F, 0x72, 0x70, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("prop")*/ 0x1D, 0x22, /*'\x221d'*/
0x6E, 0x69, 0x66, 0x6E, 0x69, 0x00, 0x00, 0x00, /*ToUInt64Key("infin")*/ 0x1E, 0x22, /*'\x221e'*/
0x67, 0x6E, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("ang")*/ 0x20, 0x22, /*'\x2220'*/
0x64, 0x6E, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("and")*/ 0x27, 0x22, /*'\x2227'*/
0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("or")*/ 0x28, 0x22, /*'\x2228'*/
0x70, 0x61, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("cap")*/ 0x29, 0x22, /*'\x2229'*/
0x70, 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("cup")*/ 0x2A, 0x22, /*'\x222a'*/
0x74, 0x6E, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("int")*/ 0x2B, 0x22, /*'\x222b'*/
0x34, 0x65, 0x72, 0x65, 0x68, 0x74, 0x00, 0x00, /*ToUInt64Key("there4")*/ 0x34, 0x22, /*'\x2234'*/
0x6D, 0x69, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sim")*/ 0x3C, 0x22, /*'\x223c'*/
0x67, 0x6E, 0x6F, 0x63, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("cong")*/ 0x45, 0x22, /*'\x2245'*/
0x70, 0x6D, 0x79, 0x73, 0x61, 0x00, 0x00, 0x00, /*ToUInt64Key("asymp")*/ 0x48, 0x22, /*'\x2248'*/
0x65, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("ne")*/ 0x60, 0x22, /*'\x2260'*/
0x76, 0x69, 0x75, 0x71, 0x65, 0x00, 0x00, 0x00, /*ToUInt64Key("equiv")*/ 0x61, 0x22, /*'\x2261'*/
0x65, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("le")*/ 0x64, 0x22, /*'\x2264'*/
0x65, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("ge")*/ 0x65, 0x22, /*'\x2265'*/
0x62, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sub")*/ 0x82, 0x22, /*'\x2282'*/
0x70, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sup")*/ 0x83, 0x22, /*'\x2283'*/
0x62, 0x75, 0x73, 0x6E, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("nsub")*/ 0x84, 0x22, /*'\x2284'*/
0x65, 0x62, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sube")*/ 0x86, 0x22, /*'\x2286'*/
0x65, 0x70, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("supe")*/ 0x87, 0x22, /*'\x2287'*/
0x73, 0x75, 0x6C, 0x70, 0x6F, 0x00, 0x00, 0x00, /*ToUInt64Key("oplus")*/ 0x95, 0x22, /*'\x2295'*/
0x73, 0x65, 0x6D, 0x69, 0x74, 0x6F, 0x00, 0x00, /*ToUInt64Key("otimes")*/ 0x97, 0x22, /*'\x2297'*/
0x70, 0x72, 0x65, 0x70, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("perp")*/ 0xA5, 0x22, /*'\x22a5'*/
0x74, 0x6F, 0x64, 0x73, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("sdot")*/ 0xC5, 0x22, /*'\x22c5'*/
0x6C, 0x69, 0x65, 0x63, 0x6C, 0x00, 0x00, 0x00, /*ToUInt64Key("lceil")*/ 0x08, 0x23, /*'\x2308'*/
0x6C, 0x69, 0x65, 0x63, 0x72, 0x00, 0x00, 0x00, /*ToUInt64Key("rceil")*/ 0x09, 0x23, /*'\x2309'*/
0x72, 0x6F, 0x6F, 0x6C, 0x66, 0x6C, 0x00, 0x00, /*ToUInt64Key("lfloor")*/ 0x0A, 0x23, /*'\x230a'*/
0x72, 0x6F, 0x6F, 0x6C, 0x66, 0x72, 0x00, 0x00, /*ToUInt64Key("rfloor")*/ 0x0B, 0x23, /*'\x230b'*/
0x67, 0x6E, 0x61, 0x6C, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("lang")*/ 0x29, 0x23, /*'\x2329'*/
0x67, 0x6E, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("rang")*/ 0x2A, 0x23, /*'\x232a'*/
0x7A, 0x6F, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, /*ToUInt64Key("loz")*/ 0xCA, 0x25, /*'\x25ca'*/
0x73, 0x65, 0x64, 0x61, 0x70, 0x73, 0x00, 0x00, /*ToUInt64Key("spades")*/ 0x60, 0x26, /*'\x2660'*/
0x73, 0x62, 0x75, 0x6C, 0x63, 0x00, 0x00, 0x00, /*ToUInt64Key("clubs")*/ 0x63, 0x26, /*'\x2663'*/
0x73, 0x74, 0x72, 0x61, 0x65, 0x68, 0x00, 0x00, /*ToUInt64Key("hearts")*/ 0x65, 0x26, /*'\x2665'*/
0x73, 0x6D, 0x61, 0x69, 0x64, 0x00, 0x00, 0x00, /*ToUInt64Key("diams")*/ 0x66, 0x26, /*'\x2666'*/
];
var dictionary = new Dictionary<ulong, char>(tableData.Length / (sizeof(ulong) + sizeof(char)));
while (tableData.Length > 0)
{
ulong key = BitConverter.ToUInt64(tableData, 0);
char value = (char)BitConverter.ToUInt16(tableData, sizeof(ulong));
dictionary[key] = value;
tableData = tableData.Skip((sizeof(ulong) + sizeof(char))).ToArray();
}
return dictionary;
}
// maps entity strings => unicode chars
private static readonly Dictionary<ulong, char> s_lookupTable = InitializeLookupTable();
public static char Lookup(char[] entity)
{
// To avoid an allocation, keys of type "ulong" are used in the lookup table.
// Since all entity strings comprise 8 characters or less and are ASCII-only, they "fit" into an ulong (8 bytes).
if (entity.Length <= 8)
{
s_lookupTable.TryGetValue(ToUInt64Key(entity), out char result);
return result;
}
else
{
// Currently, there are no entities that are longer than 8 characters.
return (char)0;
}
}
private static ulong ToUInt64Key(char[] entity)
{
// The ulong key is the reversed single-byte character representation of the actual entity string.
ulong key = 0;
for (int i = 0; i < entity.Length; i++)
{
if (entity[i] > 0xFF)
{
return 0;
}
key = (key << 8) | entity[i];
}
return key;
}
}
}
}
#endif

View File

@@ -2,12 +2,12 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.2.0</Version>
<Version>1.3.2</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
@@ -22,16 +22,25 @@
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath=""/>
<None Include="README.md" Pack="true" PackagePath="" />
</ItemGroup>
<!-- Support for old .NET versions -->
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))">
<PackageReference Include="MinValueTupleBridge" Version="0.2.1" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net40`))">
<PackageReference Include="MinAsyncBridge" Version="0.12.4" />
<PackageReference Include="MinThreadingBridge" Version="0.11.4" />
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith(`net4`))">
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SabreTools.Models" Version="1.3.0" />
</ItemGroup>
</Project>

254
Validator.cs Normal file
View File

@@ -0,0 +1,254 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.Web;
namespace SabreTools.RedumpLib
{
public static class Validator
{
/// <summary>
/// Adjust the disc type based on size and layerbreak information
/// </summary>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <returns>Corrected disc type, if possible</returns>
public static void NormalizeDiscType(SubmissionInfo info)
{
// If we have nothing valid, do nothing
if (info?.CommonDiscInfo?.Media == null || info?.SizeAndChecksums == null)
return;
switch (info.CommonDiscInfo.Media)
{
case DiscType.BD25:
case DiscType.BD33:
case DiscType.BD50:
case DiscType.BD66:
case DiscType.BD100:
case DiscType.BD128:
if (info.SizeAndChecksums.Layerbreak3 != default)
info.CommonDiscInfo.Media = DiscType.BD128;
else if (info.SizeAndChecksums.Layerbreak2 != default)
info.CommonDiscInfo.Media = DiscType.BD100;
else if (info.SizeAndChecksums.Layerbreak != default && info.SizeAndChecksums.PICIdentifier == SabreTools.Models.PIC.Constants.DiscTypeIdentifierROMUltra)
info.CommonDiscInfo.Media = DiscType.BD66;
else if (info.SizeAndChecksums.Layerbreak != default && info.SizeAndChecksums.Size > 50_050_629_632)
info.CommonDiscInfo.Media = DiscType.BD66;
else if (info.SizeAndChecksums.Layerbreak != default)
info.CommonDiscInfo.Media = DiscType.BD50;
else if (info.SizeAndChecksums.PICIdentifier == SabreTools.Models.PIC.Constants.DiscTypeIdentifierROMUltra)
info.CommonDiscInfo.Media = DiscType.BD33;
else if (info.SizeAndChecksums.Size > 25_025_314_816)
info.CommonDiscInfo.Media = DiscType.BD33;
else
info.CommonDiscInfo.Media = DiscType.BD25;
break;
case DiscType.DVD5:
case DiscType.DVD9:
if (info.SizeAndChecksums.Layerbreak != default)
info.CommonDiscInfo.Media = DiscType.DVD9;
else
info.CommonDiscInfo.Media = DiscType.DVD5;
break;
case DiscType.HDDVDSL:
case DiscType.HDDVDDL:
if (info.SizeAndChecksums.Layerbreak != default)
info.CommonDiscInfo.Media = DiscType.HDDVDDL;
else
info.CommonDiscInfo.Media = DiscType.HDDVDSL;
break;
case DiscType.UMDSL:
case DiscType.UMDDL:
if (info.SizeAndChecksums.Layerbreak != default)
info.CommonDiscInfo.Media = DiscType.UMDDL;
else
info.CommonDiscInfo.Media = DiscType.UMDSL;
break;
// All other disc types are not processed
default:
break;
}
}
/// <summary>
/// List the disc IDs associated with a given quicksearch query
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="query">Query string to attempt to search for</param>
/// <param name="filterForwardSlashes">True to filter forward slashes, false otherwise</param>
/// <returns>All disc IDs for the given query, null on error</returns>
#if NETFRAMEWORK
public async static Task<List<int>?> ListSearchResults(RedumpWebClient wc, string? query, bool filterForwardSlashes = true)
#else
public async static Task<List<int>?> ListSearchResults(RedumpHttpClient wc, string? query, bool filterForwardSlashes = true)
#endif
{
// If there is an invalid query
if (string.IsNullOrEmpty(query))
return null;
var ids = new List<int>();
// Strip quotes
query = query!.Trim('"', '\'');
// Special characters become dashes
query = query.Replace(' ', '-');
if (filterForwardSlashes)
query = query.Replace('/', '-');
query = query.Replace('\\', '/');
// Lowercase is defined per language
query = query.ToLowerInvariant();
// Keep getting quicksearch pages until there are none left
try
{
int pageNumber = 1;
while (true)
{
#if NET40
List<int> pageIds = await Task.Factory.StartNew(() => wc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++)));
#elif NETFRAMEWORK
List<int> pageIds = await Task.Run(() => wc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++)));
#else
List<int> pageIds = await wc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++));
#endif
ids.AddRange(pageIds);
if (pageIds.Count <= 1)
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"An exception occurred while trying to log in: {ex}");
return null;
}
return ids;
}
/// <summary>
/// Validate a single track against Redump, if possible
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <param name="sha1">SHA-1 hash to check against</param>
/// <returns>True if the track was found, false otherwise; List of found values, if possible</returns>
#if NETFRAMEWORK
public async static Task<(bool, List<int>?, string?)> ValidateSingleTrack(RedumpWebClient wc, SubmissionInfo info, string? sha1)
#else
public async static Task<(bool, List<int>?, string?)> ValidateSingleTrack(RedumpHttpClient wc, SubmissionInfo info, string? sha1)
#endif
{
// Get all matching IDs for the track
var newIds = await ListSearchResults(wc, sha1);
// If we got null back, there was an error
if (newIds == null)
return (false, null, "There was an unknown error retrieving information from Redump");
// If no IDs match, just return
if (!newIds.Any())
return (false, null, $"There were no matching IDs for track with SHA-1 of '{sha1}'");
// Join the list of found IDs to the existing list, if possible
if (info.PartiallyMatchedIDs != null && info.PartiallyMatchedIDs.Any())
info.PartiallyMatchedIDs.AddRange(newIds);
else
info.PartiallyMatchedIDs = newIds;
return (true, newIds, $"There were matching ID(s) found for track with SHA-1 of '{sha1}'");
}
/// <summary>
/// Validate a universal hash against Redump, if possible
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <param name="resultProgress">Optional result progress callback</param>
/// <returns>True if the track was found, false otherwise; List of found values, if possible</returns>
#if NETFRAMEWORK
public async static Task<(bool, List<int>?, string?)> ValidateUniversalHash(RedumpWebClient wc, SubmissionInfo info)
#else
public async static Task<(bool, List<int>?, string?)> ValidateUniversalHash(RedumpHttpClient wc, SubmissionInfo info)
#endif
{
// If we don't have special fields
if (info.CommonDiscInfo?.CommentsSpecialFields == null)
return (false, null, "Universal hash was missing");
// If we don't have a universal hash
string? universalHash = info.CommonDiscInfo.CommentsSpecialFields[SiteCode.UniversalHash];
if (string.IsNullOrEmpty(universalHash))
return (false, null, "Universal hash was missing");
// Format the universal hash for finding within the comments
string universalHashQuery = $"{universalHash.Substring(0, universalHash.Length - 1)}/comments/only";
// Get all matching IDs for the hash
var newIds = await ListSearchResults(wc, universalHashQuery, filterForwardSlashes: false);
// If we got null back, there was an error
if (newIds == null)
return (false, null, "There was an unknown error retrieving information from Redump");
// If no IDs match, just return
if (!newIds.Any())
return (false, null, $"There were no matching IDs for universal hash of '{universalHash}'");
// Join the list of found IDs to the existing list, if possible
if (info.PartiallyMatchedIDs != null && info.PartiallyMatchedIDs.Any())
info.PartiallyMatchedIDs.AddRange(newIds);
else
info.PartiallyMatchedIDs = newIds;
return (true, newIds, $"There were matching ID(s) found for universal hash of '{universalHash}'");
}
/// <summary>
/// Validate that the current track count and remote track count match
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="id">Redump disc ID to retrieve</param>
/// <param name="localCount">Local count of tracks for the current disc</param>
/// <returns>True if the track count matches, false otherwise</returns>
#if NETFRAMEWORK
public async static Task<bool> ValidateTrackCount(RedumpWebClient wc, int id, int localCount)
#else
public async static Task<bool> ValidateTrackCount(RedumpHttpClient wc, int id, int localCount)
#endif
{
// If we can't pull the remote data, we can't match
#if NET40
string? discData = await Task.Factory.StartNew(() => wc.DownloadSingleSiteID(id));
#elif NETFRAMEWORK
string? discData = await Task.Run(() => wc.DownloadSingleSiteID(id));
#else
string? discData = await wc.DownloadSingleSiteID(id);
#endif
if (string.IsNullOrEmpty(discData))
return false;
// Discs with only 1 track don't have a track count listed
var match = Constants.TrackCountRegex.Match(discData);
if (!match.Success && localCount == 1)
return true;
else if (!match.Success)
return false;
// If the count isn't parseable, we're not taking chances
if (!int.TryParse(match.Groups[1].Value, out int remoteCount))
return false;
// Finally check to see if the counts match
return localCount == remoteCount;
}
}
}

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -45,7 +46,7 @@ namespace SabreTools.RedumpLib.Web
public async static Task<(bool?, string?)> ValidateCredentials(string username, string password)
{
// If options are invalid or we're missing something key, just return
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return (false, null);
// Try logging in with the supplied credentials otherwise
@@ -69,16 +70,16 @@ namespace SabreTools.RedumpLib.Web
public async Task<bool?> Login(string username, string password)
{
// Credentials verification
if (!string.IsNullOrWhiteSpace(username) && !string.IsNullOrWhiteSpace(password))
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
{
Console.WriteLine("Credentials entered, will attempt Redump login...");
}
else if (!string.IsNullOrWhiteSpace(username) && string.IsNullOrWhiteSpace(password))
else if (!string.IsNullOrEmpty(username) && string.IsNullOrEmpty(password))
{
Console.WriteLine("Only a username was specified, will not attempt Redump login...");
return false;
}
else if (string.IsNullOrWhiteSpace(username))
else if (string.IsNullOrEmpty(username))
{
Console.WriteLine("No credentials entered, will not attempt Redump login...");
return false;
@@ -106,7 +107,7 @@ namespace SabreTools.RedumpLib.Web
if (response?.Content != null)
responseContent = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(responseContent))
if (string.IsNullOrEmpty(responseContent))
{
Console.WriteLine($"An error occurred while trying to log in on attempt {i}: No response");
continue;
@@ -149,7 +150,7 @@ namespace SabreTools.RedumpLib.Web
/// <returns>List of IDs from the page, empty on error</returns>
public async Task<List<int>> CheckSingleSitePage(string url)
{
List<int> ids = new();
List<int> ids = [];
// Try up to 3 times to retrieve the data
string? dumpsPage = await DownloadString(url, retries: 3);
@@ -170,7 +171,7 @@ namespace SabreTools.RedumpLib.Web
// Otherwise, traverse each dump on the page
var matches = Constants.DiscRegex.Matches(dumpsPage);
foreach (Match? match in matches)
foreach (Match? match in matches.Cast<Match?>())
{
if (match == null)
continue;
@@ -222,7 +223,7 @@ namespace SabreTools.RedumpLib.Web
// Otherwise, traverse each dump on the page
var matches = Constants.DiscRegex.Matches(dumpsPage);
foreach (Match? match in matches)
foreach (Match? match in matches.Cast<Match?>())
{
if (match == null)
continue;
@@ -253,7 +254,7 @@ namespace SabreTools.RedumpLib.Web
/// <returns>List of IDs from the page, empty on error</returns>
public async Task<List<int>> CheckSingleWIPPage(string url)
{
List<int> ids = new();
List<int> ids = [];
// Try up to 3 times to retrieve the data
string? dumpsPage = await DownloadString(url, retries: 3);
@@ -264,7 +265,7 @@ namespace SabreTools.RedumpLib.Web
// Otherwise, traverse each dump on the page
var matches = Constants.NewDiscRegex.Matches(dumpsPage);
foreach (Match? match in matches)
foreach (Match? match in matches.Cast<Match?>())
{
if (match == null)
continue;
@@ -302,7 +303,7 @@ namespace SabreTools.RedumpLib.Web
// Otherwise, traverse each dump on the page
var matches = Constants.NewDiscRegex.Matches(dumpsPage);
foreach (Match? match in matches)
foreach (Match? match in matches.Cast<Match?>())
{
if (match == null)
continue;
@@ -361,7 +362,7 @@ namespace SabreTools.RedumpLib.Web
try
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrWhiteSpace(outDir))
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string tempfile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString());
@@ -420,7 +421,7 @@ namespace SabreTools.RedumpLib.Web
public async Task<bool> DownloadSingleSiteID(int id, string? outDir, bool rename)
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrWhiteSpace(outDir))
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string paddedId = id.ToString().PadLeft(6, '0');
@@ -582,7 +583,7 @@ namespace SabreTools.RedumpLib.Web
public async Task<bool> DownloadSingleWIPID(int id, string? outDir, bool rename)
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrWhiteSpace(outDir))
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string paddedId = id.ToString().PadLeft(6, '0');
@@ -683,7 +684,7 @@ namespace SabreTools.RedumpLib.Web
// If the system is unknown, we can't do anything
string? longName = system.LongName();
if (string.IsNullOrWhiteSpace(longName))
if (string.IsNullOrEmpty(longName))
continue;
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName.Length - 1)}");
@@ -721,7 +722,7 @@ namespace SabreTools.RedumpLib.Web
// If the system is unknown, we can't do anything
string? longName = system.LongName();
if (string.IsNullOrWhiteSpace(longName))
if (string.IsNullOrEmpty(longName))
continue;
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName.Length - 1)}");
@@ -793,14 +794,14 @@ namespace SabreTools.RedumpLib.Web
private static void MoveOrDelete(string tempfile, string? newfile, string outDir, string? subfolder)
{
// If we don't have a file to move to, just delete the temp file
if (string.IsNullOrWhiteSpace(newfile))
if (string.IsNullOrEmpty(newfile))
{
File.Delete(tempfile);
return;
}
// If we have a subfolder, create it and update the newfile name
if (!string.IsNullOrWhiteSpace(subfolder))
if (!string.IsNullOrEmpty(subfolder))
{
if (!Directory.Exists(Path.Combine(outDir, subfolder)))
Directory.CreateDirectory(Path.Combine(outDir, subfolder));

View File

@@ -13,7 +13,7 @@ namespace SabreTools.RedumpLib.Web
// https://stackoverflow.com/questions/1777221/using-cookiecontainer-with-webclient-class
public class RedumpWebClient : WebClient
{
private readonly CookieContainer m_container = new CookieContainer();
private readonly CookieContainer m_container = new();
/// <summary>
/// Determines if user is logged into Redump
@@ -37,7 +37,7 @@ namespace SabreTools.RedumpLib.Web
// If we don't have the response header we care about
string headerValue = ResponseHeaders.Get("Content-Disposition");
if (string.IsNullOrWhiteSpace(headerValue))
if (string.IsNullOrEmpty(headerValue))
return null;
// Extract the filename from the value
@@ -60,20 +60,18 @@ namespace SabreTools.RedumpLib.Web
public static (bool?, string?) ValidateCredentials(string username, string password)
{
// If options are invalid or we're missing something key, just return
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return (false, null);
// Try logging in with the supplied credentials otherwise
using (RedumpWebClient wc = new RedumpWebClient())
{
bool? loggedIn = wc.Login(username, password);
if (loggedIn == true)
return (true, "Redump username and password accepted!");
else if (loggedIn == false)
return (false, "Redump username and password denied!");
else
return (null, "An error occurred validating your credentials!");
}
using RedumpWebClient wc = new();
bool? loggedIn = wc.Login(username, password);
if (loggedIn == true)
return (true, "Redump username and password accepted!");
else if (loggedIn == false)
return (false, "Redump username and password denied!");
else
return (null, "An error occurred validating your credentials!");
}
/// <summary>
@@ -85,23 +83,23 @@ namespace SabreTools.RedumpLib.Web
public bool? Login(string username, string password)
{
// Credentials verification
if (!string.IsNullOrWhiteSpace(username) && !string.IsNullOrWhiteSpace(password))
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
{
Console.WriteLine("Credentials entered, will attempt Redump login...");
}
else if (!string.IsNullOrWhiteSpace(username) && string.IsNullOrWhiteSpace(password))
else if (!string.IsNullOrEmpty(username) && string.IsNullOrEmpty(password))
{
Console.WriteLine("Only a username was specified, will not attempt Redump login...");
return false;
}
else if (string.IsNullOrWhiteSpace(username))
else if (string.IsNullOrEmpty(username))
{
Console.WriteLine("No credentials entered, will not attempt Redump login...");
return false;
}
// HTTP encode the password
#if NET40
#if NET20 || NET35 || NET40
password = Uri.EscapeUriString(password);
#else
password = WebUtility.UrlEncode(password);
@@ -156,7 +154,7 @@ namespace SabreTools.RedumpLib.Web
/// <returns>List of IDs from the page, empty on error</returns>
public List<int> CheckSingleSitePage(string url)
{
List<int> ids = new List<int>();
List<int> ids = [];
string dumpsPage = string.Empty;
// Try up to 3 times to retrieve the data
@@ -273,7 +271,7 @@ namespace SabreTools.RedumpLib.Web
/// <returns>List of IDs from the page, empty on error</returns>
public List<int> CheckSingleWIPPage(string url)
{
List<int> ids = new List<int>();
List<int> ids = [];
string dumpsPage = string.Empty;
// Try up to 3 times to retrieve the data
@@ -394,7 +392,7 @@ namespace SabreTools.RedumpLib.Web
try
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrWhiteSpace(outDir))
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string tempfile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString());
@@ -457,7 +455,7 @@ namespace SabreTools.RedumpLib.Web
public bool DownloadSingleSiteID(int id, string? outDir, bool rename)
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrWhiteSpace(outDir))
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string paddedId = id.ToString().PadLeft(6, '0');
@@ -637,7 +635,7 @@ namespace SabreTools.RedumpLib.Web
public bool DownloadSingleWIPID(int id, string? outDir, bool rename)
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrWhiteSpace(outDir))
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string paddedId = id.ToString().PadLeft(6, '0');
@@ -747,7 +745,7 @@ namespace SabreTools.RedumpLib.Web
// If the system is unknown, we can't do anything
string? longName = system.LongName();
if (string.IsNullOrWhiteSpace(longName))
if (string.IsNullOrEmpty(longName))
continue;
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName!.Length - 1)}");
@@ -785,7 +783,7 @@ namespace SabreTools.RedumpLib.Web
// If the system is unknown, we can't do anything
string? longName = system.LongName();
if (string.IsNullOrWhiteSpace(longName))
if (string.IsNullOrEmpty(longName))
continue;
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName!.Length - 1)}");
@@ -805,9 +803,9 @@ namespace SabreTools.RedumpLib.Web
/// <param name="subfolder">Optional subfolder to append to the path</param>
private static void MoveOrDelete(string tempfile, string? newfile, string outDir, string? subfolder)
{
if (!string.IsNullOrWhiteSpace(newfile))
if (!string.IsNullOrEmpty(newfile))
{
if (!string.IsNullOrWhiteSpace(subfolder))
if (!string.IsNullOrEmpty(subfolder))
{
if (!Directory.Exists(Path.Combine(outDir, subfolder)))
Directory.CreateDirectory(Path.Combine(outDir, subfolder));