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