diff --git a/SabreTools.DatTools/Rebuilder.cs b/SabreTools.DatTools/Rebuilder.cs index 58540470..baa06efa 100644 --- a/SabreTools.DatTools/Rebuilder.cs +++ b/SabreTools.DatTools/Rebuilder.cs @@ -421,10 +421,10 @@ namespace SabreTools.DatTools { // Check to see if we have a matching header first SkipperMatch.Init(); - SkipperRule rule = SkipperMatch.GetMatchingRule(fileStream, Path.GetFileNameWithoutExtension(datFile.Header.HeaderSkipper)); + Rule rule = SkipperMatch.GetMatchingRule(fileStream, Path.GetFileNameWithoutExtension(datFile.Header.HeaderSkipper)); // If there's a match, create the new file to write - if (rule.Tests != null && rule.Tests.Count != 0) + if (rule.Tests != null && rule.Tests.Length != 0) { // If the file could be transformed correctly MemoryStream transformStream = new MemoryStream(); diff --git a/SabreTools.FileTypes/BaseFile.cs b/SabreTools.FileTypes/BaseFile.cs index b379c9c9..025d8332 100644 --- a/SabreTools.FileTypes/BaseFile.cs +++ b/SabreTools.FileTypes/BaseFile.cs @@ -288,7 +288,7 @@ namespace SabreTools.FileTypes var rule = SkipperMatch.GetMatchingRule(input, Path.GetFileNameWithoutExtension(header)); // If there's a match, transform the stream before getting info - if (rule.Tests != null && rule.Tests.Count != 0) + if (rule.Tests != null && rule.Tests.Length != 0) { // Create the output stream MemoryStream outputStream = new MemoryStream(); diff --git a/SabreTools.Skippers/Detector.cs b/SabreTools.Skippers/Detector.cs new file mode 100644 index 00000000..ecc4bcef --- /dev/null +++ b/SabreTools.Skippers/Detector.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; + +namespace SabreTools.Skippers +{ + [XmlRoot("detector")] + public class Detector + { + #region Fields + + /// + /// Detector name + /// + [XmlElement("name")] + public string? Name { get; set; } + + /// + /// Author names + /// + [XmlElement("author")] + public string? Author { get; set; } + + /// + /// File version + /// + [XmlElement("version")] + public string? Version { get; set; } + + /// + /// Set of all rules in the skipper + /// + [XmlElement("rule")] + public Rule[]? Rules { get; set; } + + /// + /// Filename the skipper lives in + /// + [XmlIgnore] + public string? SourceFile { get; set; } + + #endregion + + #region Matching + + /// + /// Get the Rule associated with a given stream + /// + /// Stream to be checked + /// Name of the skipper to be used, blank to find a matching skipper + /// The Rule that matched the stream, null otherwise + public Rule? GetMatchingRule(Stream input, string skipperName) + { + // If we have no name supplied, try to blindly match + if (string.IsNullOrWhiteSpace(skipperName)) + return GetMatchingRule(input); + + // If the name matches the internal name of the skipper + else if (string.Equals(skipperName, Name, StringComparison.OrdinalIgnoreCase)) + return GetMatchingRule(input); + + // If the name matches the source file name of the skipper + else if (string.Equals(skipperName, SourceFile, StringComparison.OrdinalIgnoreCase)) + return GetMatchingRule(input); + + // Otherwise, nothing matches by default + return null; + } + + /// + /// Get the matching Rule from all Rules, if possible + /// + /// Stream to be checked + /// The Rule that matched the stream, null otherwise + private Rule? GetMatchingRule(Stream input) + { + // If we have no rules + if (Rules == null) + return null; + + // Loop through the rules until one is found that works + foreach (Rule rule in Rules) + { + // Always reset the stream back to the original place + input.Seek(0, SeekOrigin.Begin); + + // If all tests in the rule pass + if (rule.PassesAllTests(input)) + return rule; + } + + // If nothing passed + return null; + } + + #endregion + } +} diff --git a/SabreTools.Skippers/Detectors/Atari7800.cs b/SabreTools.Skippers/Detectors/Atari7800.cs new file mode 100644 index 00000000..44f3979d --- /dev/null +++ b/SabreTools.Skippers/Detectors/Atari7800.cs @@ -0,0 +1,63 @@ +using SabreTools.Skippers.Tests; + +namespace SabreTools.Skippers.Detectors +{ + /// + /// Detector for Atari 7800 headers + /// + /// Originally from a7800.xml + internal class Atari7800 : Detector + { + public Atari7800() + { + // Create tests + var rule1Test1 = new DataTest + { + Offset = "1", + Value = "415441524937383030", + Result = true, + }; + + var rule2Test1 = new DataTest + { + Offset = "64", + Value = "41435455414C20434152542044415441205354415254532048455245", + Result = true, + }; + + // Create rules + var rule1 = new Rule + { + StartOffset = "80", + EndOffset = "EOF", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule1Test1, + } + }; + + var rule2 = new Rule + { + StartOffset = "80", + EndOffset = "EOF", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule2Test1, + } + }; + + // Create file + Name = "Atari 7800"; + Author = "Roman Scherzer"; + Version = "1.0"; + SourceFile = "a7800"; + Rules = new Rule[] + { + rule1, + rule2, + }; + } + } +} diff --git a/SabreTools.Skippers/SkipperFiles/AtariLynx.cs b/SabreTools.Skippers/Detectors/AtariLynx.cs similarity index 52% rename from SabreTools.Skippers/SkipperFiles/AtariLynx.cs rename to SabreTools.Skippers/Detectors/AtariLynx.cs index 8399e9b6..68020ca0 100644 --- a/SabreTools.Skippers/SkipperFiles/AtariLynx.cs +++ b/SabreTools.Skippers/Detectors/AtariLynx.cs @@ -1,48 +1,48 @@ -using System.Collections.Generic; +using SabreTools.Skippers.Tests; -namespace SabreTools.Skippers.SkipperFiles +namespace SabreTools.Skippers.Detectors { /// - /// SkipperFile for Atari Lynx headers + /// Detector for Atari Lynx headers /// /// Originally from lynx.xml - internal class AtariLynx : SkipperFile + internal class AtariLynx : Detector { public AtariLynx() { // Create tests - var rule1Test1 = new DataSkipperTest + var rule1Test1 = new DataTest { - Offset = 0x00, - Value = new byte[] { 0x4C, 0x59, 0x4E, 0x58 }, + Offset = "0", + Value = "4C594E58", Result = true, }; - var rule2Test1 = new DataSkipperTest + var rule2Test1 = new DataTest { - Offset = 0x06, - Value = new byte[] { 0x42, 0x53, 0x39 }, + Offset = "6", + Value = "425339", Result = true, }; // Create rules - var rule1 = new SkipperRule + var rule1 = new Rule { - StartOffset = 0x40, - EndOffset = null, + StartOffset = "40", + EndOffset = "EOF", Operation = HeaderSkipOperation.None, - Tests = new List + Tests = new Test[] { rule1Test1, } }; - var rule2 = new SkipperRule + var rule2 = new Rule { - StartOffset = 0x40, - EndOffset = null, + StartOffset = "40", + EndOffset = "EOF", Operation = HeaderSkipOperation.None, - Tests = new List + Tests = new Test[] { rule2Test1, } @@ -53,7 +53,7 @@ namespace SabreTools.Skippers.SkipperFiles Author = "Roman Scherzer"; Version = "1.0"; SourceFile = "lynx"; - Rules = new List + Rules = new Rule[] { rule1, rule2, diff --git a/SabreTools.Skippers/Detectors/CommodorePSID.cs b/SabreTools.Skippers/Detectors/CommodorePSID.cs new file mode 100644 index 00000000..a95cb181 --- /dev/null +++ b/SabreTools.Skippers/Detectors/CommodorePSID.cs @@ -0,0 +1,120 @@ +using SabreTools.Skippers.Tests; + +namespace SabreTools.Skippers.Detectors +{ + /// + /// Detector for Commodore PSID headers + /// + /// Originally from psid.xml + internal class CommodorePSID : Detector + { + public CommodorePSID() + { + // Create tests + var rule1Test1 = new DataTest + { + Offset = "0", + Value = "5053494400010076", + Result = true, + }; + + var rule2Test1 = new DataTest + { + Offset = "0", + Value = "505349440003007c", + Result = true, + }; + + var rule3Test1 = new DataTest + { + Offset = "0", + Value = "505349440002007c", + Result = true, + }; + + var rule4Test1 = new DataTest + { + Offset = "0", + Value = "505349440001007c", + Result = true, + }; + + var rule5Test1 = new DataTest + { + Offset = "0", + Value = "525349440002007c", + Result = true, + }; + + // Create rules + var rule1 = new Rule + { + StartOffset = "76", + EndOffset = "EOF", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule1Test1, + } + }; + + var rule2 = new Rule + { + StartOffset = "76", + EndOffset = "EOF", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule2Test1, + } + }; + + var rule3 = new Rule + { + StartOffset = "7c", + EndOffset = "EOF", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule3Test1, + } + }; + + var rule4 = new Rule + { + StartOffset = "7c", + EndOffset = "EOF", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule4Test1, + } + }; + + var rule5 = new Rule + { + StartOffset = "7c", + EndOffset = "EOF", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule5Test1, + } + }; + + // Create file + Name = "psid"; + Author = "Yori Yoshizuki"; + Version = "1.2"; + SourceFile = "psid"; + Rules = new Rule[] + { + rule1, + rule2, + rule3, + rule4, + rule5, + }; + } + } +} diff --git a/SabreTools.Skippers/SkipperFiles/NECPCEngine.cs b/SabreTools.Skippers/Detectors/NECPCEngine.cs similarity index 53% rename from SabreTools.Skippers/SkipperFiles/NECPCEngine.cs rename to SabreTools.Skippers/Detectors/NECPCEngine.cs index a4e973fe..191bfbf5 100644 --- a/SabreTools.Skippers/SkipperFiles/NECPCEngine.cs +++ b/SabreTools.Skippers/Detectors/NECPCEngine.cs @@ -1,30 +1,29 @@ -using System.Collections.Generic; +using SabreTools.Skippers.Tests; -namespace SabreTools.Skippers.SkipperFiles +namespace SabreTools.Skippers.Detectors { /// - /// SkipperFile for NEC PC-Engine / TurboGrafx 16 headers + /// Detector for NEC PC-Engine / TurboGrafx 16 headers /// /// Originally from pce.xml - internal class NECPCEngine : SkipperFile + internal class NECPCEngine : Detector { public NECPCEngine() { // Create tests - var rule1Test1 = new DataSkipperTest + var rule1Test1 = new DataTest { - Offset = 0x00, - Value = new byte[] { 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0x02 }, + Offset = "0", + Value = "4000000000000000AABB02", Result = true, }; // Create rules - var rule1 = new SkipperRule + var rule1 = new Rule { - StartOffset = 0x200, - EndOffset = null, + StartOffset = "200", Operation = HeaderSkipOperation.None, - Tests = new List + Tests = new Test[] { rule1Test1, } @@ -35,7 +34,7 @@ namespace SabreTools.Skippers.SkipperFiles Author = "Matt Nadareski (darksabre76)"; Version = "1.0"; SourceFile = "pce"; - Rules = new List + Rules = new Rule[] { rule1, }; diff --git a/SabreTools.Skippers/SkipperFiles/Nintendo64.cs b/SabreTools.Skippers/Detectors/Nintendo64.cs similarity index 50% rename from SabreTools.Skippers/SkipperFiles/Nintendo64.cs rename to SabreTools.Skippers/Detectors/Nintendo64.cs index 2178d9e3..64e41b4a 100644 --- a/SabreTools.Skippers/SkipperFiles/Nintendo64.cs +++ b/SabreTools.Skippers/Detectors/Nintendo64.cs @@ -1,66 +1,66 @@ -using System.Collections.Generic; +using SabreTools.Skippers.Tests; -namespace SabreTools.Skippers.SkipperFiles +namespace SabreTools.Skippers.Detectors { /// - /// SkipperFile for Nintendo 64 headers + /// Detector for Nintendo 64 headers /// /// Originally from n64.xml - internal class Nintendo64 : SkipperFile + internal class Nintendo64 : Detector { public Nintendo64() { // Create tests - var rule1Test1 = new DataSkipperTest + var rule1Test1 = new DataTest { - Offset = 0x00, - Value = new byte[] { 0x80, 0x37, 0x12, 0x40 }, + Offset = "0", + Value = "80371240", Result = true, }; - var rule2Test1 = new DataSkipperTest + var rule2Test1 = new DataTest { - Offset = 0x00, - Value = new byte[] { 0x37, 0x80, 0x40, 0x12 }, + Offset = "0", + Value = "37804012", Result = true, }; - var rule3Test1 = new DataSkipperTest + var rule3Test1 = new DataTest { - Offset = 0x00, - Value = new byte[] { 0x40, 0x12, 0x37, 0x80 }, + Offset = "0", + Value = "40123780", Result = true, }; // Create rules - var rule1 = new SkipperRule + var rule1 = new Rule { - StartOffset = 0x00, - EndOffset = null, + StartOffset = "0", + EndOffset = "EOF", Operation = HeaderSkipOperation.None, - Tests = new List + Tests = new Test[] { rule1Test1, } }; - var rule2 = new SkipperRule + var rule2 = new Rule { - StartOffset = 0x00, - EndOffset = null, + StartOffset = "0", + EndOffset = "EOF", Operation = HeaderSkipOperation.Byteswap, - Tests = new List + Tests = new Test[] { rule2Test1, } }; - var rule3 = new SkipperRule + var rule3 = new Rule { - StartOffset = 0x00, - EndOffset = null, + StartOffset = "0", + EndOffset = "EOF", Operation = HeaderSkipOperation.Wordswap, - Tests = new List + Tests = new Test[] { rule3Test1, } @@ -71,7 +71,7 @@ namespace SabreTools.Skippers.SkipperFiles Author = "CUE"; Version = "1.1"; SourceFile = "n64"; - Rules = new List + Rules = new Rule[] { rule1, // V64 rule2, // Z64 diff --git a/SabreTools.Skippers/SkipperFiles/NintendoEntertainmentSystem.cs b/SabreTools.Skippers/Detectors/NintendoEntertainmentSystem.cs similarity index 54% rename from SabreTools.Skippers/SkipperFiles/NintendoEntertainmentSystem.cs rename to SabreTools.Skippers/Detectors/NintendoEntertainmentSystem.cs index 04ce1ebe..53317b2e 100644 --- a/SabreTools.Skippers/SkipperFiles/NintendoEntertainmentSystem.cs +++ b/SabreTools.Skippers/Detectors/NintendoEntertainmentSystem.cs @@ -1,30 +1,30 @@ -using System.Collections.Generic; +using SabreTools.Skippers.Tests; -namespace SabreTools.Skippers.SkipperFiles +namespace SabreTools.Skippers.Detectors { /// - /// SkipperFile for Nintendo Entertainment System headers + /// Detector for Nintendo Entertainment System headers /// /// Originally from nes.xml - internal class NintendoEntertainmentSystem : SkipperFile + internal class NintendoEntertainmentSystem : Detector { public NintendoEntertainmentSystem() { // Create tests - var rule1Test1 = new DataSkipperTest + var rule1Test1 = new DataTest { - Offset = 0x00, - Value = new byte[] { 0x4E, 0x45, 0x53, 0x1A }, + Offset = "0", + Value = "4E45531A", Result = true, }; // Create rules - var rule1 = new SkipperRule + var rule1 = new Rule { - StartOffset = 0x10, - EndOffset = null, + StartOffset = "10", + EndOffset = "EOF", Operation = HeaderSkipOperation.None, - Tests = new List + Tests = new Test[] { rule1Test1, } @@ -35,7 +35,7 @@ namespace SabreTools.Skippers.SkipperFiles Author = "Roman Scherzer"; Version = "1.1"; SourceFile = "nes"; - Rules = new List + Rules = new Rule[] { rule1, }; diff --git a/SabreTools.Skippers/Detectors/NintendoFamicomDiskSystem.cs b/SabreTools.Skippers/Detectors/NintendoFamicomDiskSystem.cs new file mode 100644 index 00000000..e197340e --- /dev/null +++ b/SabreTools.Skippers/Detectors/NintendoFamicomDiskSystem.cs @@ -0,0 +1,93 @@ +using SabreTools.Skippers.Tests; + +namespace SabreTools.Skippers.Detectors +{ + /// + /// Detector for Nintendo Famicom Disk System headers + /// + /// Originally from fds.xml + internal class NintendoFamicomDiskSystem : Detector + { + public NintendoFamicomDiskSystem() + { + // Create tests + var rule1Test1 = new DataTest + { + Offset = "0", + Value = "4644531A010000000000000000000000", + }; + + var rule2Test1 = new DataTest + { + Offset = "0", + Value = "4644531A020000000000000000000000", + }; + + var rule3Test1 = new DataTest + { + Offset = "0", + Value = "4644531A030000000000000000000000", + }; + + var rule4Test1 = new DataTest + { + Offset = "0", + Value = "4644531A040000000000000000000000", + }; + + // Create rules + var rule1 = new Rule + { + StartOffset = "10", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule1Test1, + } + }; + + var rule2 = new Rule + { + StartOffset = "10", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule2Test1, + } + }; + + var rule3 = new Rule + { + StartOffset = "10", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule3Test1, + } + }; + + var rule4 = new Rule + { + StartOffset = "10", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule4Test1, + } + }; + + // Create file + Name = "fds"; + Author = "Yori Yoshizuki"; + Version = "1.0"; + SourceFile = "fds"; + Rules = new Rule[] + { + rule1, + rule2, + rule3, + rule4, + }; + } + } +} diff --git a/SabreTools.Skippers/SkipperFiles/SuperFamicomSPC.cs b/SabreTools.Skippers/Detectors/SuperFamicomSPC.cs similarity index 54% rename from SabreTools.Skippers/SkipperFiles/SuperFamicomSPC.cs rename to SabreTools.Skippers/Detectors/SuperFamicomSPC.cs index ffd677f3..31c9ad71 100644 --- a/SabreTools.Skippers/SkipperFiles/SuperFamicomSPC.cs +++ b/SabreTools.Skippers/Detectors/SuperFamicomSPC.cs @@ -1,30 +1,30 @@ -using System.Collections.Generic; +using SabreTools.Skippers.Tests; -namespace SabreTools.Skippers.SkipperFiles +namespace SabreTools.Skippers.Detectors { /// - /// SkipperFile for Super Famicom SPC headers + /// Detector for Super Famicom SPC headers /// /// Originally from spc.xml - internal class SuperFamicomSPC : SkipperFile + internal class SuperFamicomSPC : Detector { public SuperFamicomSPC() { // Create tests - var rule1Test1 = new DataSkipperTest + var rule1Test1 = new DataTest { - Offset = 0x00, - Value = new byte[] { 0x53, 0x4E, 0x45, 0x53, 0x2D, 0x53, 0x50, 0x43 }, + Offset = "0", + Value = "534E45532D535043", Result = true, }; // Create rules - var rule1 = new SkipperRule + var rule1 = new Rule { - StartOffset = 0x100, - EndOffset = null, + StartOffset = "00100", + EndOffset = "EOF", Operation = HeaderSkipOperation.None, - Tests = new List + Tests = new Test[] { rule1Test1, } @@ -35,7 +35,7 @@ namespace SabreTools.Skippers.SkipperFiles Author = "Yori Yoshizuki"; Version = "1.0"; SourceFile = "spc"; - Rules = new List + Rules = new Rule[] { rule1, }; diff --git a/SabreTools.Skippers/Detectors/SuperNintendoEntertainmentSystem.cs b/SabreTools.Skippers/Detectors/SuperNintendoEntertainmentSystem.cs new file mode 100644 index 00000000..b66a0ac0 --- /dev/null +++ b/SabreTools.Skippers/Detectors/SuperNintendoEntertainmentSystem.cs @@ -0,0 +1,76 @@ +using SabreTools.Skippers.Tests; + +namespace SabreTools.Skippers.Detectors +{ + /// + /// Detector for Super Nintendo Entertainment System headers + /// + /// Originally from snes.xml + internal class SuperNintendoEntertainmentSystem : Detector + { + public SuperNintendoEntertainmentSystem() + { + // Create tests + var rule1Test1 = new DataTest + { + Offset = "16", + Value = "0000000000000000", + }; + + var rule2Test1 = new DataTest + { + Offset = "16", + Value = "AABB040000000000", + }; + + var rule3Test1 = new DataTest + { + Offset = "16", + Value = "535550455255464F", + }; + + // Create rules + var rule1 = new Rule + { + StartOffset = "200", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule1Test1, + } + }; + + var rule2 = new Rule + { + StartOffset = "200", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule2Test1, + } + }; + + var rule3 = new Rule + { + StartOffset = "200", + Operation = HeaderSkipOperation.None, + Tests = new Test[] + { + rule3Test1, + } + }; + + // Create file + Name = "Nintendo Super Famicom/SNES"; + Author = "Matt Nadareski (darksabre76)"; + Version = "1.0"; + SourceFile = "snes"; + Rules = new Rule[] + { + rule1, // FIG + rule2, // SMC + rule3, // UFO + }; + } + } +} diff --git a/SabreTools.Skippers/Enums.cs b/SabreTools.Skippers/Enums.cs index 1d102c18..da602977 100644 --- a/SabreTools.Skippers/Enums.cs +++ b/SabreTools.Skippers/Enums.cs @@ -7,6 +7,7 @@ namespace SabreTools.Skippers /// public enum HeaderSkipOperation { + [XmlEnum("none")] None = 0, [XmlEnum("bitswap")] diff --git a/SabreTools.Skippers/SkipperRule.cs b/SabreTools.Skippers/Rule.cs similarity index 70% rename from SabreTools.Skippers/SkipperRule.cs rename to SabreTools.Skippers/Rule.cs index 0187e1be..8ad04bfb 100644 --- a/SabreTools.Skippers/SkipperRule.cs +++ b/SabreTools.Skippers/Rule.cs @@ -1,27 +1,49 @@ using System; -using System.Collections.Generic; using System.IO; using System.Xml.Serialization; - using SabreTools.Logging; +using SabreTools.Skippers.Tests; namespace SabreTools.Skippers { - public class SkipperRule + [XmlType("rule")] + public class Rule { #region Fields /// /// Starting offset for applying rule /// + /// Either numeric or the literal "EOF" [XmlAttribute("start_offset")] - public long? StartOffset { get; set; } // null is EOF + public string? StartOffset + { + get => _startOffset == null ? "EOF" : _startOffset.Value.ToString(); + set + { + if (value == null || value.ToLowerInvariant() == "eof") + _startOffset = null; + else + _startOffset = Convert.ToInt64(value, fromBase: 16); + } + } /// /// Ending offset for applying rule /// + /// Either numeric or the literal "EOF" [XmlAttribute("end_offset")] - public long? EndOffset { get; set; } // null if EOF + public string? EndOffset + { + get => _endOffset == null ? "EOF" : _endOffset.Value.ToString(); + set + { + if (value == null || value.ToLowerInvariant() == "eof") + _endOffset = null; + else + _endOffset = Convert.ToInt64(value, fromBase: 16); + } + } /// /// Byte manipulation operation @@ -32,19 +54,34 @@ namespace SabreTools.Skippers /// /// List of matching tests in a rule /// - [XmlArray] - [XmlArrayItem("data")] - [XmlArrayItem("or")] - [XmlArrayItem("xor")] - [XmlArrayItem("and")] - [XmlArrayItem("file")] - public List Tests { get; set; } + [XmlElement("and", typeof(AndTest))] + [XmlElement("data", typeof(DataTest))] + [XmlElement("file", typeof(FileTest))] + [XmlElement("or", typeof(OrTest))] + [XmlElement("xor", typeof(XorTest))] + public Test[]? Tests { get; set; } /// /// Filename the skipper rule lives in /// [XmlIgnore] - public string SourceFile { get; set; } + public string? SourceFile { get; set; } + + #endregion + + #region Private instance variables + + /// + /// Starting offset for applying rule + /// + /// null is EOF + private long? _startOffset = null; + + /// + /// Ending offset for applying rule + /// + /// null is EOF + private long? _endOffset = null; #endregion @@ -57,27 +94,25 @@ namespace SabreTools.Skippers #endregion - #region Constructors - - /// - /// Constructor - /// - public SkipperRule() + public Rule() { logger = new Logger(this); } - #endregion - /// - /// Check if a Stream passes all tests in the SkipperRule + /// Check if a Stream passes all tests in the Rule /// /// Stream to check /// True if all tests passed, false otherwise public bool PassesAllTests(Stream input) { bool success = true; - foreach (SkipperTest test in Tests) + + // If there are no tests + if (Tests == null || Tests.Length == 0) + return success; + + foreach (Test test in Tests) { bool result = test.Passes(input); success &= result; @@ -94,16 +129,23 @@ namespace SabreTools.Skippers /// True if the file was transformed properly, false otherwise public bool TransformFile(string input, string output) { - // If the input file doesn't exist, fail - if (!File.Exists(input)) + // If the input file doesn't exist + if (string.IsNullOrWhiteSpace(input) || !File.Exists(input)) { - logger.Error($"I'm sorry but '{input}' doesn't exist!"); + logger.Error($"'{input}' doesn't exist and cannot be transformed!"); + return false; + } + + // If we have an invalid output directory name + if (string.IsNullOrWhiteSpace(output)) + { + logger.Error($"Output path was null or empty, cannot write transformed file!"); return false; } // Create the output directory if it doesn't already - if (!Directory.Exists(Path.GetDirectoryName(output))) - Directory.CreateDirectory(Path.GetDirectoryName(output)); + string parentDirectory = Path.GetDirectoryName(output) ?? string.Empty; + Directory.CreateDirectory(parentDirectory); //logger.User($"Attempting to apply rule to '{input}'"); bool success = TransformStream(File.OpenRead(input), File.Create(output)); @@ -134,15 +176,15 @@ namespace SabreTools.Skippers long extsize = input.Length; if ((Operation > HeaderSkipOperation.Bitswap && (extsize % 2) != 0) || (Operation > HeaderSkipOperation.Byteswap && (extsize % 4) != 0) - || (Operation > HeaderSkipOperation.Bitswap && (StartOffset == null || StartOffset % 2 != 0))) + || (Operation > HeaderSkipOperation.Bitswap && (_startOffset == null || _startOffset % 2 != 0))) { logger.Error("The stream did not have the correct size to be transformed!"); return false; } // Now read the proper part of the file and apply the rule - BinaryWriter bw = null; - BinaryReader br = null; + BinaryWriter? bw = null; + BinaryReader? br = null; try { logger.User("Applying found rule to input stream"); @@ -150,24 +192,24 @@ namespace SabreTools.Skippers br = new BinaryReader(input); // Seek to the beginning offset - if (StartOffset == null) + if (_startOffset == null) success = false; - else if (Math.Abs((long)StartOffset) > input.Length) + else if (Math.Abs((long)_startOffset) > input.Length) success = false; - else if (StartOffset > 0) - input.Seek((long)StartOffset, SeekOrigin.Begin); + else if (_startOffset > 0) + input.Seek((long)_startOffset, SeekOrigin.Begin); - else if (StartOffset < 0) - input.Seek((long)StartOffset, SeekOrigin.End); + else if (_startOffset < 0) + input.Seek((long)_startOffset, SeekOrigin.End); // Then read and apply the operation as you go if (success) { byte[] buffer = new byte[4]; int pos = 0; - while (input.Position < (EndOffset ?? input.Length) + while (input.Position < (_endOffset ?? input.Length) && input.Position < input.Length) { byte b = br.ReadByte(); diff --git a/SabreTools.Skippers/SabreTools.Skippers.csproj b/SabreTools.Skippers/SabreTools.Skippers.csproj index 24d1232c..5f458e10 100644 --- a/SabreTools.Skippers/SabreTools.Skippers.csproj +++ b/SabreTools.Skippers/SabreTools.Skippers.csproj @@ -2,6 +2,7 @@ net6.0;net7.0 + enable diff --git a/SabreTools.Skippers/SkipperFile.cs b/SabreTools.Skippers/SkipperFile.cs deleted file mode 100644 index f15ba2ee..00000000 --- a/SabreTools.Skippers/SkipperFile.cs +++ /dev/null @@ -1,416 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace SabreTools.Skippers -{ - /// - /// It is well worth considering just moving the XML files to code, similar to how RV does it - /// if only because nobody really has any skippers outside of this. It would also make the - /// output directory cleaner and less prone to user error in case something didn't get copied - /// correctly. The contents of these files should still be added to the wiki, in that case. - /// - public class SkipperFile - { - #region Fields - - /// - /// Skipper name - /// - [XmlElement("name")] - public string Name { get; set; } = string.Empty; - - /// - /// Author names - /// - [XmlElement("author")] - public string Author { get; set; } = string.Empty; - - /// - /// File version - /// - [XmlElement("version")] - public string Version { get; set; } = string.Empty; - - /// - /// Set of all rules in the skipper - /// - [XmlArray("rule")] - public List Rules { get; set; } = new List(); - - /// - /// Filename the skipper lives in - /// - [XmlIgnore] - public string SourceFile { get; set; } = string.Empty; - - #endregion - - #region Constructors - - /// - /// Create an empty SkipperFile object - /// - public SkipperFile() { } - - /// - /// Create a SkipperFile object parsed from an input file - /// - /// Name of the file to parse - public SkipperFile(string filename) - { - Rules = new List(); - SourceFile = Path.GetFileNameWithoutExtension(filename); - - XmlReader xtr = XmlReader.Create(filename, new XmlReaderSettings - { - CheckCharacters = false, - DtdProcessing = DtdProcessing.Ignore, - IgnoreComments = true, - IgnoreWhitespace = true, - ValidationFlags = XmlSchemaValidationFlags.None, - ValidationType = ValidationType.None, - }); - - bool valid = Parse(xtr); - - // If we somehow have an invalid file, zero out the fields - if (!valid) - { - Name = null; - Author = null; - Version = null; - Rules = null; - SourceFile = null; - } - } - - #endregion - - #region Parsing Helpers - - /// - /// Parse an XML document in as a SkipperFile - /// - /// XmlReader representing the document - /// True if the file could be parsed, false otherwise - private bool Parse(XmlReader xtr) - { - if (xtr == null) - return false; - - try - { - bool valid = false; - xtr.MoveToContent(); - while (!xtr.EOF) - { - if (xtr.NodeType != XmlNodeType.Element) - xtr.Read(); - - switch (xtr.Name.ToLowerInvariant()) - { - case "detector": - valid = true; - xtr.Read(); - break; - - case "name": - Name = xtr.ReadElementContentAsString(); - break; - - case "author": - Author = xtr.ReadElementContentAsString(); - break; - - case "version": - Version = xtr.ReadElementContentAsString(); - break; - - case "rule": - SkipperRule rule = ParseRule(xtr); - if (rule != null) - Rules.Add(rule); - - xtr.Read(); - break; - - default: - xtr.Read(); - break; - } - } - - return valid; - } - catch - { - return false; - } - } - - /// - /// Parse an XML document in as a SkipperRule - /// - /// XmlReader representing the document - /// Filled SkipperRule on success, null otherwise - private SkipperRule ParseRule(XmlReader xtr) - { - if (xtr == null) - return null; - - try - { - // Get the information from the rule first - SkipperRule rule = new SkipperRule - { - StartOffset = null, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List(), - SourceFile = this.SourceFile, - }; - - string startOffset = xtr.GetAttribute("start_offset"); - if (startOffset != null) - { - if (startOffset.ToLowerInvariant() == "eof") - rule.StartOffset = null; - else - rule.StartOffset = Convert.ToInt64(startOffset, 16); - } - - string endOffset = xtr.GetAttribute("end_offset"); - if (endOffset != null) - { - if (endOffset.ToLowerInvariant() == "eof") - rule.EndOffset = null; - else - rule.EndOffset = Convert.ToInt64(endOffset, 16); - } - - string operation = xtr.GetAttribute("operation"); - if (operation != null) - { - switch (operation.ToLowerInvariant()) - { - case "bitswap": - rule.Operation = HeaderSkipOperation.Bitswap; - break; - case "byteswap": - rule.Operation = HeaderSkipOperation.Byteswap; - break; - case "wordswap": - rule.Operation = HeaderSkipOperation.Wordswap; - break; - case "wordbyteswap": - rule.Operation = HeaderSkipOperation.WordByteswap; - break; - } - } - - // Now read the individual tests into the Rule - XmlReader subreader = xtr.ReadSubtree(); - if (subreader != null) - { - subreader.MoveToContent(); - while (!subreader.EOF) - { - if (subreader.NodeType != XmlNodeType.Element) - subreader.Read(); - - switch (xtr.Name.ToLowerInvariant()) - { - case "data": - case "or": - case "xor": - case "and": - case "file": - SkipperTest test = ParseTest(subreader); - if (test != null) - rule.Tests.Add(test); - - subreader.Read(); - break; - - default: - subreader.Read(); - break; - } - } - } - - return rule; - } - catch - { - return null; - } - } - - /// - /// Parse an XML document in as a SkipperTest - /// - /// XmlReader representing the document - /// Filled SkipperTest on success, null otherwise - private SkipperTest ParseTest(XmlReader xtr) - { - if (xtr == null) - return null; - - try - { - // Get the test type - SkipperTest test = xtr.Name.ToLowerInvariant() switch - { - "and" => new AndSkipperTest(), - "data" => new DataSkipperTest(), - "file" => new FileSkipperTest(), - "or" => new OrSkipperTest(), - "xor" => new XorSkipperTest(), - _ => null, - }; - - // If we had an invalid test type - if (test == null) - return null; - - // Set the default values - test.Offset = 0; - test.Value = Array.Empty(); - test.Result = true; - test.Mask = Array.Empty(); - test.Size = 0; - test.Operator = HeaderSkipTestFileOperator.Equal; - - // Now populate all the parts that we can - if (xtr.GetAttribute("offset") != null) - { - string offset = xtr.GetAttribute("offset"); - if (offset.ToLowerInvariant() == "eof") - test.Offset = null; - else - test.Offset = Convert.ToInt64(offset, 16); - } - - if (xtr.GetAttribute("value") != null) - { - string value = xtr.GetAttribute("value"); - - // http://stackoverflow.com/questions/321370/how-can-i-convert-a-hex-string-to-a-byte-array - test.Value = new byte[value.Length / 2]; - for (int index = 0; index < test.Value.Length; index++) - { - string byteValue = value.Substring(index * 2, 2); - test.Value[index] = byte.Parse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture); - } - } - - if (xtr.GetAttribute("result") != null) - { - string result = xtr.GetAttribute("result"); - if (!bool.TryParse(result, out bool resultBool)) - resultBool = true; - - test.Result = resultBool; - } - - if (xtr.GetAttribute("mask") != null) - { - string mask = xtr.GetAttribute("mask"); - - // http://stackoverflow.com/questions/321370/how-can-i-convert-a-hex-string-to-a-byte-array - test.Mask = new byte[mask.Length / 2]; - for (int index = 0; index < test.Mask.Length; index++) - { - string byteValue = mask.Substring(index * 2, 2); - test.Mask[index] = byte.Parse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture); - } - } - - if (xtr.GetAttribute("size") != null) - { - string size = xtr.GetAttribute("size"); - if (size.ToLowerInvariant() == "po2") - test.Size = null; - else - test.Size = Convert.ToInt64(size, 16); - } - - if (xtr.GetAttribute("operator") != null) - { - string oper = xtr.GetAttribute("operator"); - test.Operator = oper.ToLowerInvariant() switch - { - "less" => HeaderSkipTestFileOperator.Less, - "greater" => HeaderSkipTestFileOperator.Greater, - "equal" => HeaderSkipTestFileOperator.Equal, - _ => HeaderSkipTestFileOperator.Equal, - }; - } - - return test; - } - catch - { - return null; - } - } - - #endregion - - #region Matching - - /// - /// Get the SkipperRule associated with a given stream - /// - /// Stream to be checked - /// Name of the skipper to be used, blank to find a matching skipper - /// The SkipperRule that matched the stream, null otherwise - public SkipperRule GetMatchingRule(Stream input, string skipperName) - { - // If we have no name supplied, try to blindly match - if (string.IsNullOrWhiteSpace(skipperName)) - return GetMatchingRule(input); - - // If the name matches the internal name of the skipper - else if (string.Equals(skipperName, Name, StringComparison.OrdinalIgnoreCase)) - return GetMatchingRule(input); - - // If the name matches the source file name of the skipper - else if (string.Equals(skipperName, SourceFile, StringComparison.OrdinalIgnoreCase)) - return GetMatchingRule(input); - - // Otherwise, nothing matches by default - return null; - } - - /// - /// Get the matching SkipperRule from all Rules, if possible - /// - /// Stream to be checked - /// The SkipperRule that matched the stream, null otherwise - private SkipperRule GetMatchingRule(Stream input) - { - // Loop through the rules until one is found that works - foreach (SkipperRule rule in Rules) - { - // Always reset the stream back to the original place - input.Seek(0, SeekOrigin.Begin); - - // If all tests in the rule pass, we return this rule - if (rule.PassesAllTests(input)) - return rule; - } - - // If nothing passed, we return null by default - return null; - } - - #endregion - } -} diff --git a/SabreTools.Skippers/SkipperFiles/Atari7800.cs b/SabreTools.Skippers/SkipperFiles/Atari7800.cs deleted file mode 100644 index 9f196194..00000000 --- a/SabreTools.Skippers/SkipperFiles/Atari7800.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; - -namespace SabreTools.Skippers.SkipperFiles -{ - /// - /// SkipperFile for Atari 7800 headers - /// - /// Originally from a7800.xml - internal class Atari7800 : SkipperFile - { - public Atari7800() - { - // Create tests - var rule1Test1 = new DataSkipperTest - { - Offset = 0x01, - Value = new byte[] { 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30 }, - Result = true, - }; - - var rule2Test1 = new DataSkipperTest - { - Offset = 0x64, - Value = new byte[] { 0x41, 0x43, 0x54, 0x55, 0x41, 0x4C, 0x20, 0x43, 0x41, 0x52, 0x54, 0x20, 0x44, 0x41, 0x54, 0x41, 0x20, 0x53, 0x54, 0x41, 0x52, 0x54, 0x53, 0x20, 0x48, 0x45, 0x52, 0x45 }, - Result = true, - }; - - // Create rules - var rule1 = new SkipperRule - { - StartOffset = 0x80, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule1Test1, - } - }; - - var rule2 = new SkipperRule - { - StartOffset = 0x80, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule2Test1, - } - }; - - // Create file - Name = "Atari 7800"; - Author = "Roman Scherzer"; - Version = "1.0"; - SourceFile = "a7800"; - Rules = new List - { - rule1, - rule2, - }; - } - } -} diff --git a/SabreTools.Skippers/SkipperFiles/CommodorePSID.cs b/SabreTools.Skippers/SkipperFiles/CommodorePSID.cs deleted file mode 100644 index 46ac4740..00000000 --- a/SabreTools.Skippers/SkipperFiles/CommodorePSID.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Collections.Generic; - -namespace SabreTools.Skippers.SkipperFiles -{ - /// - /// SkipperFile for Commodore PSID headers - /// - /// Originally from psid.xml - internal class CommodorePSID : SkipperFile - { - public CommodorePSID() - { - // Create tests - var rule1Test1 = new DataSkipperTest - { - Offset = 0x00, - Value = new byte[] { 0x50, 0x53, 0x49, 0x44, 0x00, 0x01, 0x00, 0x76 }, - Result = true, - }; - - var rule2Test1 = new DataSkipperTest - { - Offset = 0x00, - Value = new byte[] { 0x50, 0x53, 0x49, 0x44, 0x00, 0x03, 0x00, 0x7c }, - Result = true, - }; - - var rule3Test1 = new DataSkipperTest - { - Offset = 0x00, - Value = new byte[] { 0x50, 0x53, 0x49, 0x44, 0x00, 0x02, 0x00, 0x7c }, - Result = true, - }; - - var rule4Test1 = new DataSkipperTest - { - Offset = 0x00, - Value = new byte[] { 0x50, 0x53, 0x49, 0x44, 0x00, 0x01, 0x00, 0x7c }, - Result = true, - }; - - var rule5Test1 = new DataSkipperTest - { - Offset = 0x00, - Value = new byte[] { 0x52, 0x53, 0x49, 0x44, 0x00, 0x02, 0x00, 0x7c }, - Result = true, - }; - - // Create rules - var rule1 = new SkipperRule - { - StartOffset = 0x76, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule1Test1, - } - }; - - var rule2 = new SkipperRule - { - StartOffset = 0x76, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule2Test1, - } - }; - - var rule3 = new SkipperRule - { - StartOffset = 0x7c, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule3Test1, - } - }; - - var rule4 = new SkipperRule - { - StartOffset = 0x7c, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule4Test1, - } - }; - - var rule5 = new SkipperRule - { - StartOffset = 0x7c, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule5Test1, - } - }; - - // Create file - Name = "psid"; - Author = "Yori Yoshizuki"; - Version = "1.2"; - SourceFile = "psid"; - Rules = new List - { - rule1, - rule2, - rule3, - rule4, - rule5, - }; - } - } -} diff --git a/SabreTools.Skippers/SkipperFiles/NintendoFamicomDiskSystem.cs b/SabreTools.Skippers/SkipperFiles/NintendoFamicomDiskSystem.cs deleted file mode 100644 index 9708441c..00000000 --- a/SabreTools.Skippers/SkipperFiles/NintendoFamicomDiskSystem.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; - -namespace SabreTools.Skippers.SkipperFiles -{ - /// - /// SkipperFile for Nintendo Famicom Disk System headers - /// - /// Originally from fds.xml - internal class NintendoFamicomDiskSystem : SkipperFile - { - public NintendoFamicomDiskSystem() - { - // Create tests - var rule1Test1 = new DataSkipperTest - { - Offset = 0x00, - Value = new byte[] { 0x46, 0x44, 0x53, 0x1A, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, - Result = true, - }; - - var rule2Test1 = new DataSkipperTest - { - Offset = 0x00, - Value = new byte[] { 0x46, 0x44, 0x53, 0x1A, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, - Result = true, - }; - - var rule3Test1 = new DataSkipperTest - { - Offset = 0x00, - Value = new byte[] { 0x46, 0x44, 0x53, 0x1A, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, - Result = true, - }; - - var rule4Test1 = new DataSkipperTest - { - Offset = 0x00, - Value = new byte[] { 0x46, 0x44, 0x53, 0x1A, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, - Result = true, - }; - - // Create rules - var rule1 = new SkipperRule - { - StartOffset = 0x10, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule1Test1, - } - }; - - var rule2 = new SkipperRule - { - StartOffset = 0x10, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule2Test1, - } - }; - - var rule3 = new SkipperRule - { - StartOffset = 0x10, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule3Test1, - } - }; - - var rule4 = new SkipperRule - { - StartOffset = 0x10, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule4Test1, - } - }; - - // Create file - Name = "fds"; - Author = "Yori Yoshizuki"; - Version = "1.0"; - SourceFile = "fds"; - Rules = new List - { - rule1, - rule2, - rule3, - rule4, - }; - } - } -} diff --git a/SabreTools.Skippers/SkipperFiles/SuperNintendoEntertainmentSystem.cs b/SabreTools.Skippers/SkipperFiles/SuperNintendoEntertainmentSystem.cs deleted file mode 100644 index 24ea94ce..00000000 --- a/SabreTools.Skippers/SkipperFiles/SuperNintendoEntertainmentSystem.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Collections.Generic; - -namespace SabreTools.Skippers.SkipperFiles -{ - /// - /// SkipperFile for Super Nintendo Entertainment System headers - /// - /// Originally from snes.xml - internal class SuperNintendoEntertainmentSystem : SkipperFile - { - public SuperNintendoEntertainmentSystem() - { - // Create tests - var rule1Test1 = new DataSkipperTest - { - Offset = 0x16, - Value = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, - Result = true, - }; - - var rule2Test1 = new DataSkipperTest - { - Offset = 0x16, - Value = new byte[] { 0xAA, 0xBB, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 }, - Result = true, - }; - - var rule3Test1 = new DataSkipperTest - { - Offset = 0x16, - Value = new byte[] { 0x53, 0x55, 0x50, 0x45, 0x52, 0x55, 0x46, 0x4F }, - Result = true, - }; - - // Create rules - var rule1 = new SkipperRule - { - StartOffset = 0x200, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule1Test1, - } - }; - - var rule2 = new SkipperRule - { - StartOffset = 0x200, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule2Test1, - } - }; - - var rule3 = new SkipperRule - { - StartOffset = 0x200, - EndOffset = null, - Operation = HeaderSkipOperation.None, - Tests = new List - { - rule3Test1, - } - }; - - // Create file - Name = "Nintendo Super Famicom/SNES"; - Author = "Matt Nadareski (darksabre76)"; - Version = "1.0"; - SourceFile = "snes"; - Rules = new List - { - rule1, // FIG - rule2, // SMC - rule3, // UFO - }; - } - } -} diff --git a/SabreTools.Skippers/SkipperMatch.cs b/SabreTools.Skippers/SkipperMatch.cs index 745c2df6..7292f94f 100644 --- a/SabreTools.Skippers/SkipperMatch.cs +++ b/SabreTools.Skippers/SkipperMatch.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; using System.IO; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; using SabreTools.IO; using SabreTools.Logging; @@ -23,9 +26,9 @@ namespace SabreTools.Skippers public static class SkipperMatch { /// - /// Header skippers represented by a list of skipper objects + /// Header detectors represented by a list of detector objects /// - private static List Skippers = null; + private static List? Skippers = null; /// /// Local paths @@ -37,7 +40,7 @@ namespace SabreTools.Skippers /// /// Logging object /// - private static readonly Logger logger = new Logger(); + private static readonly Logger logger = new(); #endregion @@ -70,13 +73,43 @@ namespace SabreTools.Skippers private static void PopulateSkippers() { // Ensure the list exists - if (Skippers == null) - Skippers = new List(); + Skippers ??= new List(); + + // Create the XML serializer + var xts = new XmlSerializer(typeof(Detector)); // Get skippers for each known header type - foreach (string skipperFile in Directory.EnumerateFiles(LocalPath, "*", SearchOption.AllDirectories)) + foreach (string skipperPath in Directory.EnumerateFiles(LocalPath, "*", SearchOption.AllDirectories)) { - Skippers.Add(new SkipperFile(Path.GetFullPath(skipperFile))); + 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) + continue; + + // Set the source file + string sourceFile = Path.GetFileNameWithoutExtension(skipperPath); + detector.SourceFile = sourceFile; + for (int i = 0; i < (detector.Rules?.Length ?? 0); i++) + { + detector.Rules[i].SourceFile = sourceFile; + } + + // Add the skipper to the set + Skippers.Add(detector); + } + catch { } } } @@ -90,63 +123,64 @@ namespace SabreTools.Skippers private static void PopulateSkippersInternal() { // Ensure the list exists - if (Skippers == null) - Skippers = new List(); + Skippers ??= new List(); // Get skippers for each known header type - Skippers.Add(new SkipperFiles.Atari7800()); - Skippers.Add(new SkipperFiles.AtariLynx()); - Skippers.Add(new SkipperFiles.CommodorePSID()); - Skippers.Add(new SkipperFiles.NECPCEngine()); - Skippers.Add(new SkipperFiles.Nintendo64()); - Skippers.Add(new SkipperFiles.NintendoEntertainmentSystem()); - Skippers.Add(new SkipperFiles.NintendoFamicomDiskSystem()); - Skippers.Add(new SkipperFiles.SuperNintendoEntertainmentSystem()); - Skippers.Add(new SkipperFiles.SuperFamicomSPC()); + 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 SkipperRule associated with a given file + /// 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 SkipperRule that matched the file - public static SkipperRule GetMatchingRule(string input, string skipperName) + /// 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 SkipperRule(); + return new Rule(); } return GetMatchingRule(File.OpenRead(input), skipperName); } /// - /// Get the SkipperRule associated with a given stream + /// 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 SkipperRule that matched the file - public static SkipperRule GetMatchingRule(Stream input, string skipperName, bool keepOpen = false) + /// The Rule that matched the file + public static Rule GetMatchingRule(Stream input, string skipperName, bool keepOpen = false) { - SkipperRule skipperRule = new SkipperRule(); + var skipperRule = new Rule(); - // If we have a null skipper name, we return since we're not matching skippers - if (skipperName == null) + // 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"); - List tempList = new List(); - tempList.AddRange(Skippers); - // Loop through all known SkipperFiles - foreach (SkipperFile skipper in tempList) + // 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; @@ -156,9 +190,8 @@ namespace SabreTools.Skippers if (!keepOpen) input.Dispose(); - // If the SkipperRule is null, make it empty - if (skipperRule == null) - skipperRule = new SkipperRule(); + // If the Rule is null, make it empty + skipperRule ??= new Rule(); // If we have a blank rule, inform the user if (skipperRule.Tests == null) diff --git a/SabreTools.Skippers/SkipperTest.cs b/SabreTools.Skippers/SkipperTest.cs deleted file mode 100644 index dec415ba..00000000 --- a/SabreTools.Skippers/SkipperTest.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace SabreTools.Skippers -{ - /// - /// Individual test that applies to a SkipperRule - /// - public abstract class SkipperTest - { - #region Fields - - /// - /// File offset to run the test - /// - /// null is EOF - [XmlAttribute("offset")] - public long? Offset { get; set; } - - /// - /// Static value to be checked at the offset - /// - [XmlAttribute("value")] - public byte[] Value { get; set; } - - /// - /// Determines whether a pass or failure is expected - /// - [XmlAttribute("result")] - public bool Result { get; set; } - - /// - /// Byte mask to be applied to the tested bytes - /// - [XmlAttribute("mask")] - public byte[] Mask { get; set; } - - /// - /// Expected size of the input byte array, used with the Operator - /// - [XmlAttribute("size")] - public long? Size { get; set; } // null is PO2, "power of 2" filesize - - /// - /// Expected range value for the input byte array size, used with Size - /// - [XmlAttribute("operator")] - public HeaderSkipTestFileOperator Operator { get; set; } - - #endregion - - /// - /// Check if a stream passes the test - /// - /// Stream to check rule against - /// The Stream is assumed to be in the proper position for a given test - public abstract bool Passes(Stream input); - - #region Checking Helpers - - /// - /// Seek an input stream based on the test value - /// - /// Stream to seek - /// True if the stream could seek, false on error - protected bool Seek(Stream input) - { - try - { - // Null offset means EOF - if (Offset == null) - input.Seek(0, SeekOrigin.End); - - // Positive offset means from beginning - else if (Offset >= 0 && Offset <= input.Length) - input.Seek(Offset.Value, SeekOrigin.Begin); - - // Negative offset means from end - else if (Offset < 0 && Math.Abs(Offset.Value) <= input.Length) - input.Seek(Offset.Value, SeekOrigin.End); - - return true; - } - catch - { - return false; - } - } - - #endregion - } - - /// - /// Skipper test using AND - /// - [XmlType("and")] - public class AndSkipperTest : SkipperTest - { - /// - public override bool Passes(Stream input) - { - // First seek to the correct position - Seek(input); - - bool result = true; - try - { - // Then apply the mask if it exists - byte[] read = new byte[Mask.Length]; - input.Read(read, 0, Mask.Length); - - byte[] masked = new byte[Mask.Length]; - for (int i = 0; i < read.Length; i++) - { - masked[i] = (byte)(read[i] & Mask[i]); - } - - // Finally, compare it against the value - for (int i = 0; i < Value.Length; i++) - { - if (masked[i] != Value[i]) - { - result = false; - break; - } - } - } - catch - { - result = false; - } - - // Return if the expected and actual results match - return result == Result; - } - } - - /// - /// Skipper test using DATA - /// - [XmlType("data")] - public class DataSkipperTest : SkipperTest - { - /// - public override bool Passes(Stream input) - { - // First seek to the correct position - if (!Seek(input)) - return false; - - // Then read and compare bytewise - bool result = true; - for (int i = 0; i < Value.Length; i++) - { - try - { - if (input.ReadByte() != Value[i]) - { - result = false; - break; - } - } - catch - { - result = false; - break; - } - } - - // Return if the expected and actual results match - return result == Result; - } - } - - /// - /// Skipper test using FILE - /// - [XmlType("file")] - public class FileSkipperTest : SkipperTest - { - /// - public override bool Passes(Stream input) - { - // First get the file size from stream - long size = input.Length; - - // If we have a null size, check that the size is a power of 2 - bool result = true; - if (Size == null) - { - // http://stackoverflow.com/questions/600293/how-to-check-if-a-number-is-a-power-of-2 - result = (((ulong)size & ((ulong)size - 1)) == 0); - } - else if (Operator == HeaderSkipTestFileOperator.Less) - { - result = (size < Size); - } - else if (Operator == HeaderSkipTestFileOperator.Greater) - { - result = (size > Size); - } - else if (Operator == HeaderSkipTestFileOperator.Equal) - { - result = (size == Size); - } - - // Return if the expected and actual results match - return result == Result; - } - } - - /// - /// Skipper test using OR - /// - [XmlType("or")] - public class OrSkipperTest : SkipperTest - { - /// - public override bool Passes(Stream input) - { - // First seek to the correct position - Seek(input); - - bool result = true; - try - { - // Then apply the mask if it exists - byte[] read = new byte[Mask.Length]; - input.Read(read, 0, Mask.Length); - - byte[] masked = new byte[Mask.Length]; - for (int i = 0; i < read.Length; i++) - { - masked[i] = (byte)(read[i] | Mask[i]); - } - - // Finally, compare it against the value - for (int i = 0; i < Value.Length; i++) - { - if (masked[i] != Value[i]) - { - result = false; - break; - } - } - } - catch - { - result = false; - } - - // Return if the expected and actual results match - return result == Result; - } - } - - /// - /// Skipper test using XOR - /// - [XmlType("xor")] - public class XorSkipperTest : SkipperTest - { - /// - public override bool Passes(Stream input) - { - // First seek to the correct position - Seek(input); - - bool result = true; - try - { - // Then apply the mask if it exists - byte[] read = new byte[Mask.Length]; - input.Read(read, 0, Mask.Length); - - byte[] masked = new byte[Mask.Length]; - for (int i = 0; i < read.Length; i++) - { - masked[i] = (byte)(read[i] ^ Mask[i]); - } - - // Finally, compare it against the value - for (int i = 0; i < Value.Length; i++) - { - if (masked[i] != Value[i]) - { - result = false; - break; - } - } - } - catch - { - result = false; - } - - // Return if the expected and actual results match - return result == Result; - } - } -} \ No newline at end of file diff --git a/SabreTools.Skippers/Test.cs b/SabreTools.Skippers/Test.cs new file mode 100644 index 00000000..53db15d1 --- /dev/null +++ b/SabreTools.Skippers/Test.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; +using System.IO; + +namespace SabreTools.Skippers +{ + /// + /// Individual test that applies to a Rule + /// + public abstract class Test + { + /// + /// Check if a stream passes the test + /// + /// Stream to check rule against + /// The Stream is assumed to be in the proper position for a given test + public abstract bool Passes(Stream input); + + #region Helpers + + /// + /// Prase a hex string into a byte array + /// + /// + protected static byte[]? ParseByteArrayFromHex(string? hex) + { + // If we have an invalid string + if (string.IsNullOrWhiteSpace(hex)) + return null; + + var ret = new byte[hex.Length / 2]; + for (int index = 0; index < ret.Length; index++) + { + string byteValue = hex.Substring(index * 2, 2); + ret[index] = byte.Parse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + + return ret; + } + + /// + /// Seek an input stream based on the test value + /// + /// Stream to seek + /// Offset to seek to + /// True if the stream could seek, false on error + protected static bool Seek(Stream input, long? offset) + { + try + { + // Null offset means EOF + if (offset == null) + input.Seek(0, SeekOrigin.End); + + // Positive offset means from beginning + else if (offset >= 0 && offset <= input.Length) + input.Seek(offset.Value, SeekOrigin.Begin); + + // Negative offset means from end + else if (offset < 0 && Math.Abs(offset.Value) <= input.Length) + input.Seek(offset.Value, SeekOrigin.End); + + return true; + } + catch + { + return false; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/SabreTools.Skippers/Tests/AndTest.cs b/SabreTools.Skippers/Tests/AndTest.cs new file mode 100644 index 00000000..82f63e61 --- /dev/null +++ b/SabreTools.Skippers/Tests/AndTest.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; + +namespace SabreTools.Skippers.Tests +{ + /// + /// Test that uses a byte mask AND against data + /// + [XmlType("and")] + public class AndTest : Test + { + #region Fields + + /// + /// File offset to run the test + /// + /// Either numeric or the literal "EOF" + [XmlAttribute("offset")] + public string? Offset + { + get => _offset == null ? "EOF" : _offset.Value.ToString(); + set + { + if (value == null || value.ToLowerInvariant() == "eof") + _offset = null; + else + _offset = Convert.ToInt64(value, fromBase: 16); + } + } + + /// + /// Static value to be checked at the offset + /// + /// Hex string representation of a byte array + [XmlAttribute("value")] + public string? Value + { + get => _value == null ? string.Empty : BitConverter.ToString(_value).Replace("-", string.Empty); + set => _value = ParseByteArrayFromHex(value); + } + + /// + /// Determines whether a pass or failure is expected + /// + [XmlAttribute("result")] + public bool Result { get; set; } = true; + + /// + /// Byte mask to be applied to the tested bytes + /// + /// Hex string representation of a byte array + [XmlAttribute("mask")] + public string? Mask + { + get => _mask == null ? string.Empty : BitConverter.ToString(_mask).Replace("-", string.Empty); + set => _mask = ParseByteArrayFromHex(value); + } + + #endregion + + #region Private instance variables + + /// + /// File offset to run the test + /// + /// null is EOF + private long? _offset; + + /// + /// Static value to be checked at the offset + /// + private byte[]? _value; + + /// + /// Byte mask to be applied to the tested bytes + /// + private byte[]? _mask; + + #endregion + + /// + public override bool Passes(Stream input) + { + // If we have an invalid mask + if (_mask == null || _mask.Length == 0) + return false; + + // If we have an invalid value + if (_value == null || _value.Length == 0) + return false; + + // Seek to the correct position, if possible + if (!Seek(input, _offset)) + return false; + + bool result = true; + try + { + // Then apply the mask if it exists + byte[] read = new byte[_mask.Length]; + input.Read(read, 0, _mask.Length); + + byte[] masked = new byte[_mask.Length]; + for (int i = 0; i < read.Length; i++) + { + masked[i] = (byte)(read[i] & _mask[i]); + } + + // Finally, compare it against the value + for (int i = 0; i < _value.Length; i++) + { + if (masked[i] != _value[i]) + { + result = false; + break; + } + } + } + catch + { + result = false; + } + + // Return if the expected and actual results match + return result == Result; + } + } +} \ No newline at end of file diff --git a/SabreTools.Skippers/Tests/DataTest.cs b/SabreTools.Skippers/Tests/DataTest.cs new file mode 100644 index 00000000..2187dffd --- /dev/null +++ b/SabreTools.Skippers/Tests/DataTest.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; + +namespace SabreTools.Skippers.Tests +{ + /// + /// Test that checks data matches + /// + [XmlType("data")] + public class DataTest : Test + { + #region Fields + + /// + /// File offset to run the test + /// + /// Either numeric or the literal "EOF" + [XmlAttribute("offset")] + public string? Offset + { + get => _offset == null ? "EOF" : _offset.Value.ToString(); + set + { + if (value == null || value.ToLowerInvariant() == "eof") + _offset = null; + else + _offset = Convert.ToInt64(value, fromBase: 16); + } + } + + /// + /// Static value to be checked at the offset + /// + /// Hex string representation of a byte array + [XmlAttribute("value")] + public string? Value + { + get => _value == null ? string.Empty : BitConverter.ToString(_value).Replace("-", string.Empty); + set => _value = ParseByteArrayFromHex(value); + } + + /// + /// Determines whether a pass or failure is expected + /// + [XmlAttribute("result")] + public bool Result { get; set; } = true; + + #endregion + + #region Private instance variables + + /// + /// File offset to run the test + /// + /// null is EOF + private long? _offset; + + /// + /// Static value to be checked at the offset + /// + private byte[]? _value; + + #endregion + + /// + public override bool Passes(Stream input) + { + // If we have an invalid value + if (_value == null || _value.Length == 0) + return false; + + // Seek to the correct position, if possible + if (!Seek(input, _offset)) + return false; + + // Then read and compare bytewise + bool result = true; + for (int i = 0; i < _value.Length; i++) + { + try + { + if (input.ReadByte() != _value[i]) + { + result = false; + break; + } + } + catch + { + result = false; + break; + } + } + + // Return if the expected and actual results match + return result == Result; + } + } +} \ No newline at end of file diff --git a/SabreTools.Skippers/Tests/FileTest.cs b/SabreTools.Skippers/Tests/FileTest.cs new file mode 100644 index 00000000..231c3f2d --- /dev/null +++ b/SabreTools.Skippers/Tests/FileTest.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; + +namespace SabreTools.Skippers.Tests +{ + /// + /// Test that tests file size + /// + [XmlType("file")] + public class FileTest : Test + { + #region Fields + + /// + /// Determines whether a pass or failure is expected + /// + [XmlAttribute("result")] + public bool Result { get; set; } = true; + + /// + /// Expected size of the input byte array, used with the Operator + /// + /// Either numeric or the literal "po2" + [XmlAttribute("size")] + public string Size + { + get => _size == null ? "po2" : _size.Value.ToString(); + set + { + if (value == null || value.ToLowerInvariant() == "po2") + _size = null; + else + _size = Convert.ToInt64(value, fromBase: 16); + } + } + + /// + /// Expected range value for the input byte array size, used with Size + /// + [XmlAttribute("operator")] + public HeaderSkipTestFileOperator Operator { get; set; } + + #endregion + + #region Private instance variables + + /// + /// File offset to run the test + /// + /// null is PO2 ("power of 2" filesize) + private long? _size; + + #endregion + + /// + public override bool Passes(Stream input) + { + // First get the file size from stream + long size = input.Length; + + // If we have a null size, check that the size is a power of 2 + bool result = true; + if (_size == null) + { + // http://stackoverflow.com/questions/600293/how-to-check-if-a-number-is-a-power-of-2 + result = (((ulong)size & ((ulong)size - 1)) == 0); + } + else if (Operator == HeaderSkipTestFileOperator.Less) + { + result = (size < _size); + } + else if (Operator == HeaderSkipTestFileOperator.Greater) + { + result = (size > _size); + } + else if (Operator == HeaderSkipTestFileOperator.Equal) + { + result = (size == _size); + } + + // Return if the expected and actual results match + return result == Result; + } + } +} \ No newline at end of file diff --git a/SabreTools.Skippers/Tests/OrTest.cs b/SabreTools.Skippers/Tests/OrTest.cs new file mode 100644 index 00000000..22d6818d --- /dev/null +++ b/SabreTools.Skippers/Tests/OrTest.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; + +namespace SabreTools.Skippers.Tests +{ + /// + /// Test that uses a byte mask OR against data + /// + [XmlType("or")] + public class OrTest : Test + { + #region Fields + + /// + /// File offset to run the test + /// + /// Either numeric or the literal "EOF" + [XmlAttribute("offset")] + public string? Offset + { + get => _offset == null ? "EOF" : _offset.Value.ToString(); + set + { + if (value == null || value.ToLowerInvariant() == "eof") + _offset = null; + else + _offset = Convert.ToInt64(value, fromBase: 16); + } + } + + /// + /// Static value to be checked at the offset + /// + /// Hex string representation of a byte array + [XmlAttribute("value")] + public string? Value + { + get => _value == null ? string.Empty : BitConverter.ToString(_value).Replace("-", string.Empty); + set => _value = ParseByteArrayFromHex(value); + } + + /// + /// Determines whether a pass or failure is expected + /// + [XmlAttribute("result")] + public bool Result { get; set; } = true; + + /// + /// Byte mask to be applied to the tested bytes + /// + /// Hex string representation of a byte array + [XmlAttribute("mask")] + public string? Mask + { + get => _mask == null ? string.Empty : BitConverter.ToString(_mask).Replace("-", string.Empty); + set => _mask = ParseByteArrayFromHex(value); + } + + #endregion + + #region Private instance variables + + /// + /// File offset to run the test + /// + /// null is EOF + private long? _offset; + + /// + /// Static value to be checked at the offset + /// + private byte[]? _value; + + /// + /// Byte mask to be applied to the tested bytes + /// + private byte[]? _mask; + + #endregion + + /// + public override bool Passes(Stream input) + { + // If we have an invalid mask + if (_mask == null || _mask.Length == 0) + return false; + + // If we have an invalid value + if (_value == null || _value.Length == 0) + return false; + + // Seek to the correct position, if possible + if (!Seek(input, _offset)) + return false; + + bool result = true; + try + { + // Then apply the mask if it exists + byte[] read = new byte[_mask.Length]; + input.Read(read, 0, _mask.Length); + + byte[] masked = new byte[_mask.Length]; + for (int i = 0; i < read.Length; i++) + { + masked[i] = (byte)(read[i] | _mask[i]); + } + + // Finally, compare it against the value + for (int i = 0; i < _value.Length; i++) + { + if (masked[i] != _value[i]) + { + result = false; + break; + } + } + } + catch + { + result = false; + } + + // Return if the expected and actual results match + return result == Result; + } + } +} \ No newline at end of file diff --git a/SabreTools.Skippers/Tests/XorTest.cs b/SabreTools.Skippers/Tests/XorTest.cs new file mode 100644 index 00000000..b8852576 --- /dev/null +++ b/SabreTools.Skippers/Tests/XorTest.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; + +namespace SabreTools.Skippers.Tests +{ + /// + /// Test that uses a byte mask XOR against data + /// + [XmlType("xor")] + public class XorTest : Test + { + #region Fields + + /// + /// File offset to run the test + /// + /// Either numeric or the literal "EOF" + [XmlAttribute("offset")] + public string? Offset + { + get => _offset == null ? "EOF" : _offset.Value.ToString(); + set + { + if (value == null || value.ToLowerInvariant() == "eof") + _offset = null; + else + _offset = Convert.ToInt64(value, fromBase: 16); + } + } + + /// + /// Static value to be checked at the offset + /// + /// Hex string representation of a byte array + [XmlAttribute("value")] + public string? Value + { + get => _value == null ? string.Empty : BitConverter.ToString(_value).Replace("-", string.Empty); + set => _value = ParseByteArrayFromHex(value); + } + + /// + /// Determines whether a pass or failure is expected + /// + [XmlAttribute("result")] + public bool Result { get; set; } = true; + + /// + /// Byte mask to be applied to the tested bytes + /// + /// Hex string representation of a byte array + [XmlAttribute("mask")] + public string? Mask + { + get => _mask == null ? string.Empty : BitConverter.ToString(_mask).Replace("-", string.Empty); + set => _mask = ParseByteArrayFromHex(value); + } + + #endregion + + #region Private instance variables + + /// + /// File offset to run the test + /// + /// null is EOF + private long? _offset; + + /// + /// Static value to be checked at the offset + /// + private byte[]? _value; + + /// + /// Byte mask to be applied to the tested bytes + /// + private byte[]? _mask; + + #endregion + + /// + public override bool Passes(Stream input) + { + // If we have an invalid mask + if (_mask == null || _mask.Length == 0) + return false; + + // If we have an invalid value + if (_value == null || _value.Length == 0) + return false; + + // Seek to the correct position, if possible + if (!Seek(input, _offset)) + return false; + + bool result = true; + try + { + // Then apply the mask if it exists + byte[] read = new byte[_mask.Length]; + input.Read(read, 0, _mask.Length); + + byte[] masked = new byte[_mask.Length]; + for (int i = 0; i < read.Length; i++) + { + masked[i] = (byte)(read[i] ^ _mask[i]); + } + + // Finally, compare it against the value + for (int i = 0; i < _value.Length; i++) + { + if (masked[i] != _value[i]) + { + result = false; + break; + } + } + } + catch + { + result = false; + } + + // Return if the expected and actual results match + return result == Result; + } + } +} \ No newline at end of file diff --git a/SabreTools.Test/Skippers/SkipperRuleTransformTests.cs b/SabreTools.Test/Skippers/SkipperRuleTransformTests.cs index 2d1dc9d6..2928093c 100644 --- a/SabreTools.Test/Skippers/SkipperRuleTransformTests.cs +++ b/SabreTools.Test/Skippers/SkipperRuleTransformTests.cs @@ -6,9 +6,9 @@ using Xunit; namespace SabreTools.Test.Skippers { [Collection("SkipperMatch")] - public class SkipperRuleTransformTests + public class RuleTransformTests { - public SkipperRuleTransformTests() + public RuleTransformTests() { SkipperMatch.Init(false); } @@ -231,7 +231,7 @@ namespace SabreTools.Test.Skippers /// /// Pad the stream to 1KiB then seek to beginning /// - private void PadAndReset(MemoryStream ms) + private static void PadAndReset(MemoryStream ms) { for (long i = ms.Length; i < 1024; i++) { diff --git a/SabreTools/Features/Extract.cs b/SabreTools/Features/Extract.cs index ea19c391..4e6bf9a3 100644 --- a/SabreTools/Features/Extract.cs +++ b/SabreTools/Features/Extract.cs @@ -77,10 +77,10 @@ The following systems have headers that this program can work with: // Get the skipper rule that matches the file, if any SkipperMatch.Init(); - SkipperRule rule = SkipperMatch.GetMatchingRule(file, string.Empty); + Rule rule = SkipperMatch.GetMatchingRule(file, string.Empty); // If we have an empty rule, return false - if (rule.Tests == null || rule.Tests.Count == 0 || rule.Operation != HeaderSkipOperation.None) + if (rule.Tests == null || rule.Tests.Length == 0 || rule.Operation != HeaderSkipOperation.None) return false; logger.User("File has a valid copier header"); @@ -91,8 +91,8 @@ The following systems have headers that this program can work with: { // Extract the header as a string for the database using var fs = File.OpenRead(file); - byte[] hbin = new byte[(int)rule.StartOffset]; - fs.Read(hbin, 0, (int)rule.StartOffset); + byte[] hbin = new byte[int.Parse(rule.StartOffset)]; + fs.Read(hbin, 0, int.Parse(rule.StartOffset)); hstr = Utilities.ByteArrayToString(hbin); } catch