using System.Collections.Generic; using System.IO; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; using SabreTools.IO; using SabreTools.Logging; namespace SabreTools.Skippers { /// /// Class for matching existing Skippers /// /// /// Skippers, in general, are distributed as XML files by some projects /// in order to denote a way of transforming a file so that it will match /// the hashes included in their DATs. Each skipper file can contain multiple /// skipper rules, each of which denote a type of header/transformation. In /// turn, each of those rules can contain multiple tests that denote that /// a file should be processed using that rule. Transformations can include /// simply skipping over a portion of the file all the way to byteswapping /// the entire file. For the purposes of this library, Skippers also denote /// a way of changing files directly in order to produce a file whose external /// hash would match those same DATs. /// public static class SkipperMatch { /// /// Header detectors represented by a list of detector objects /// private static List? Skippers = null; /// /// Local paths /// private static readonly string LocalPath = Path.Combine(PathTool.GetRuntimeDirectory(), "Skippers") + Path.DirectorySeparatorChar; #region Logging /// /// Logging object /// private static readonly Logger logger = new(); #endregion /// /// Initialize static fields /// /// True to enable internal header skipper generation, false to use file-based generation (default) public static void Init(bool experimental = false) { // If the list is populated, don't add to it if (Skippers != null) return; // If we're using internal skipper generation if (experimental) PopulateSkippersInternal(); // If we're using file-based skipper generation else PopulateSkippers(); } /// /// Populate the entire list of header skippers from physical files /// /// /// http://mamedev.emulab.it/clrmamepro/docs/xmlheaders.txt /// http://www.emulab.it/forum/index.php?topic=127.0 /// private static void PopulateSkippers() { // Ensure the list exists Skippers ??= new List(); // Create the XML serializer var xts = new XmlSerializer(typeof(Detector)); // Get skippers for each known header type foreach (string skipperPath in Directory.EnumerateFiles(LocalPath, "*", SearchOption.AllDirectories)) { try { // Create the XML reader var xtr = XmlReader.Create(skipperPath, new XmlReaderSettings { CheckCharacters = false, DtdProcessing = DtdProcessing.Ignore, IgnoreComments = true, IgnoreWhitespace = true, ValidationFlags = XmlSchemaValidationFlags.None, ValidationType = ValidationType.None, }); // Deserialize the detector, if possible if (xts.Deserialize(xtr) is not Detector detector || detector == null) continue; // Set the source file on the detector string sourceFile = Path.GetFileNameWithoutExtension(skipperPath); detector.SourceFile = sourceFile; // Set the source file on the rules if (detector.Rules != null) { for (int i = 0; i < detector.Rules.Length; i++) { if (detector.Rules[i] == null) continue; detector.Rules[i].SourceFile = sourceFile; } } // Add the skipper to the set Skippers.Add(detector); } catch { } } } /// /// Populate the entire list of header skippers from generated objects /// /// /// http://mamedev.emulab.it/clrmamepro/docs/xmlheaders.txt /// http://www.emulab.it/forum/index.php?topic=127.0 /// private static void PopulateSkippersInternal() { // Ensure the list exists Skippers ??= new List(); // Get skippers for each known header type Skippers.Add(new Detectors.Atari7800()); Skippers.Add(new Detectors.AtariLynx()); Skippers.Add(new Detectors.CommodorePSID()); Skippers.Add(new Detectors.NECPCEngine()); Skippers.Add(new Detectors.Nintendo64()); Skippers.Add(new Detectors.NintendoEntertainmentSystem()); Skippers.Add(new Detectors.NintendoFamicomDiskSystem()); Skippers.Add(new Detectors.SuperNintendoEntertainmentSystem()); Skippers.Add(new Detectors.SuperFamicomSPC()); } /// /// Get the Rule associated with a given file /// /// Name of the file to be checked /// Name of the skipper to be used, blank to find a matching skipper /// Logger object for file and console output /// The Rule that matched the file public static Rule GetMatchingRule(string input, string skipperName) { // If the file doesn't exist, return a blank skipper rule if (!File.Exists(input)) { logger.Error($"The file '{input}' does not exist so it cannot be tested"); return new Rule(); } return GetMatchingRule(File.OpenRead(input), skipperName); } /// /// Get the Rule associated with a given stream /// /// Name of the file to be checked /// Name of the skipper to be used, blank to find a matching skipper /// True if the underlying stream should be kept open, false otherwise /// The Rule that matched the file public static Rule GetMatchingRule(Stream input, string skipperName, bool keepOpen = false) { var skipperRule = new Rule(); // If we have an invalid set of skippers or skipper name if (Skippers == null || skipperName == null) return skipperRule; // Loop through and find a Skipper that has the right name logger.Verbose("Beginning search for matching header skip rules"); // Loop through all known Detectors foreach (Detector? skipper in Skippers) { // This should not happen if (skipper == null) continue; skipperRule = skipper.GetMatchingRule(input, skipperName); if (skipperRule != null) break; } // If we're not keeping the stream open, dispose of the binary reader if (!keepOpen) input.Dispose(); // If the Rule is null, make it empty skipperRule ??= new Rule(); // If we have a blank rule, inform the user if (skipperRule.Tests == null) logger.Verbose("No matching rule found!"); else logger.User("Matching rule found!"); return skipperRule; } } }