Complete overhaul on Skippers

This change involves safety updates for serialization, better definitions of various classes, renames of some classes for accuracy, missing enum decoration, and various fixes.
This commit is contained in:
Matt Nadareski
2023-04-04 18:31:19 -04:00
parent 573aa2848a
commit 01ce52ec35
30 changed files with 1342 additions and 1249 deletions

View File

@@ -421,10 +421,10 @@ namespace SabreTools.DatTools
{ {
// Check to see if we have a matching header first // Check to see if we have a matching header first
SkipperMatch.Init(); 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 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 // If the file could be transformed correctly
MemoryStream transformStream = new MemoryStream(); MemoryStream transformStream = new MemoryStream();

View File

@@ -288,7 +288,7 @@ namespace SabreTools.FileTypes
var rule = SkipperMatch.GetMatchingRule(input, Path.GetFileNameWithoutExtension(header)); var rule = SkipperMatch.GetMatchingRule(input, Path.GetFileNameWithoutExtension(header));
// If there's a match, transform the stream before getting info // 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 // Create the output stream
MemoryStream outputStream = new MemoryStream(); MemoryStream outputStream = new MemoryStream();

View File

@@ -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
/// <summary>
/// Detector name
/// </summary>
[XmlElement("name")]
public string? Name { get; set; }
/// <summary>
/// Author names
/// </summary>
[XmlElement("author")]
public string? Author { get; set; }
/// <summary>
/// File version
/// </summary>
[XmlElement("version")]
public string? Version { get; set; }
/// <summary>
/// Set of all rules in the skipper
/// </summary>
[XmlElement("rule")]
public Rule[]? Rules { get; set; }
/// <summary>
/// Filename the skipper lives in
/// </summary>
[XmlIgnore]
public string? SourceFile { get; set; }
#endregion
#region Matching
/// <summary>
/// Get the Rule associated with a given stream
/// </summary>
/// <param name="input">Stream to be checked</param>
/// <param name="skipperName">Name of the skipper to be used, blank to find a matching skipper</param>
/// <returns>The Rule that matched the stream, null otherwise</returns>
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;
}
/// <summary>
/// Get the matching Rule from all Rules, if possible
/// </summary>
/// <param name="input">Stream to be checked</param>
/// <returns>The Rule that matched the stream, null otherwise</returns>
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
}
}

View File

@@ -0,0 +1,63 @@
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.Detectors
{
/// <summary>
/// Detector for Atari 7800 headers
/// </summary>
/// <remarks>Originally from a7800.xml</remarks>
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,
};
}
}
}

View File

@@ -1,48 +1,48 @@
using System.Collections.Generic; using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.SkipperFiles namespace SabreTools.Skippers.Detectors
{ {
/// <summary> /// <summary>
/// SkipperFile for Atari Lynx headers /// Detector for Atari Lynx headers
/// </summary> /// </summary>
/// <remarks>Originally from lynx.xml</remarks> /// <remarks>Originally from lynx.xml</remarks>
internal class AtariLynx : SkipperFile internal class AtariLynx : Detector
{ {
public AtariLynx() public AtariLynx()
{ {
// Create tests // Create tests
var rule1Test1 = new DataSkipperTest var rule1Test1 = new DataTest
{ {
Offset = 0x00, Offset = "0",
Value = new byte[] { 0x4C, 0x59, 0x4E, 0x58 }, Value = "4C594E58",
Result = true, Result = true,
}; };
var rule2Test1 = new DataSkipperTest var rule2Test1 = new DataTest
{ {
Offset = 0x06, Offset = "6",
Value = new byte[] { 0x42, 0x53, 0x39 }, Value = "425339",
Result = true, Result = true,
}; };
// Create rules // Create rules
var rule1 = new SkipperRule var rule1 = new Rule
{ {
StartOffset = 0x40, StartOffset = "40",
EndOffset = null, EndOffset = "EOF",
Operation = HeaderSkipOperation.None, Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest> Tests = new Test[]
{ {
rule1Test1, rule1Test1,
} }
}; };
var rule2 = new SkipperRule var rule2 = new Rule
{ {
StartOffset = 0x40, StartOffset = "40",
EndOffset = null, EndOffset = "EOF",
Operation = HeaderSkipOperation.None, Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest> Tests = new Test[]
{ {
rule2Test1, rule2Test1,
} }
@@ -53,7 +53,7 @@ namespace SabreTools.Skippers.SkipperFiles
Author = "Roman Scherzer"; Author = "Roman Scherzer";
Version = "1.0"; Version = "1.0";
SourceFile = "lynx"; SourceFile = "lynx";
Rules = new List<SkipperRule> Rules = new Rule[]
{ {
rule1, rule1,
rule2, rule2,

View File

@@ -0,0 +1,120 @@
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.Detectors
{
/// <summary>
/// Detector for Commodore PSID headers
/// </summary>
/// <remarks>Originally from psid.xml</remarks>
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,
};
}
}
}

View File

@@ -1,30 +1,29 @@
using System.Collections.Generic; using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.SkipperFiles namespace SabreTools.Skippers.Detectors
{ {
/// <summary> /// <summary>
/// SkipperFile for NEC PC-Engine / TurboGrafx 16 headers /// Detector for NEC PC-Engine / TurboGrafx 16 headers
/// </summary> /// </summary>
/// <remarks>Originally from pce.xml</remarks> /// <remarks>Originally from pce.xml</remarks>
internal class NECPCEngine : SkipperFile internal class NECPCEngine : Detector
{ {
public NECPCEngine() public NECPCEngine()
{ {
// Create tests // Create tests
var rule1Test1 = new DataSkipperTest var rule1Test1 = new DataTest
{ {
Offset = 0x00, Offset = "0",
Value = new byte[] { 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0x02 }, Value = "4000000000000000AABB02",
Result = true, Result = true,
}; };
// Create rules // Create rules
var rule1 = new SkipperRule var rule1 = new Rule
{ {
StartOffset = 0x200, StartOffset = "200",
EndOffset = null,
Operation = HeaderSkipOperation.None, Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest> Tests = new Test[]
{ {
rule1Test1, rule1Test1,
} }
@@ -35,7 +34,7 @@ namespace SabreTools.Skippers.SkipperFiles
Author = "Matt Nadareski (darksabre76)"; Author = "Matt Nadareski (darksabre76)";
Version = "1.0"; Version = "1.0";
SourceFile = "pce"; SourceFile = "pce";
Rules = new List<SkipperRule> Rules = new Rule[]
{ {
rule1, rule1,
}; };

View File

@@ -1,66 +1,66 @@
using System.Collections.Generic; using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.SkipperFiles namespace SabreTools.Skippers.Detectors
{ {
/// <summary> /// <summary>
/// SkipperFile for Nintendo 64 headers /// Detector for Nintendo 64 headers
/// </summary> /// </summary>
/// <remarks>Originally from n64.xml</remarks> /// <remarks>Originally from n64.xml</remarks>
internal class Nintendo64 : SkipperFile internal class Nintendo64 : Detector
{ {
public Nintendo64() public Nintendo64()
{ {
// Create tests // Create tests
var rule1Test1 = new DataSkipperTest var rule1Test1 = new DataTest
{ {
Offset = 0x00, Offset = "0",
Value = new byte[] { 0x80, 0x37, 0x12, 0x40 }, Value = "80371240",
Result = true, Result = true,
}; };
var rule2Test1 = new DataSkipperTest var rule2Test1 = new DataTest
{ {
Offset = 0x00, Offset = "0",
Value = new byte[] { 0x37, 0x80, 0x40, 0x12 }, Value = "37804012",
Result = true, Result = true,
}; };
var rule3Test1 = new DataSkipperTest var rule3Test1 = new DataTest
{ {
Offset = 0x00, Offset = "0",
Value = new byte[] { 0x40, 0x12, 0x37, 0x80 }, Value = "40123780",
Result = true, Result = true,
}; };
// Create rules // Create rules
var rule1 = new SkipperRule var rule1 = new Rule
{ {
StartOffset = 0x00, StartOffset = "0",
EndOffset = null, EndOffset = "EOF",
Operation = HeaderSkipOperation.None, Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest> Tests = new Test[]
{ {
rule1Test1, rule1Test1,
} }
}; };
var rule2 = new SkipperRule var rule2 = new Rule
{ {
StartOffset = 0x00, StartOffset = "0",
EndOffset = null, EndOffset = "EOF",
Operation = HeaderSkipOperation.Byteswap, Operation = HeaderSkipOperation.Byteswap,
Tests = new List<SkipperTest> Tests = new Test[]
{ {
rule2Test1, rule2Test1,
} }
}; };
var rule3 = new SkipperRule var rule3 = new Rule
{ {
StartOffset = 0x00, StartOffset = "0",
EndOffset = null, EndOffset = "EOF",
Operation = HeaderSkipOperation.Wordswap, Operation = HeaderSkipOperation.Wordswap,
Tests = new List<SkipperTest> Tests = new Test[]
{ {
rule3Test1, rule3Test1,
} }
@@ -71,7 +71,7 @@ namespace SabreTools.Skippers.SkipperFiles
Author = "CUE"; Author = "CUE";
Version = "1.1"; Version = "1.1";
SourceFile = "n64"; SourceFile = "n64";
Rules = new List<SkipperRule> Rules = new Rule[]
{ {
rule1, // V64 rule1, // V64
rule2, // Z64 rule2, // Z64

View File

@@ -1,30 +1,30 @@
using System.Collections.Generic; using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.SkipperFiles namespace SabreTools.Skippers.Detectors
{ {
/// <summary> /// <summary>
/// SkipperFile for Nintendo Entertainment System headers /// Detector for Nintendo Entertainment System headers
/// </summary> /// </summary>
/// <remarks>Originally from nes.xml</remarks> /// <remarks>Originally from nes.xml</remarks>
internal class NintendoEntertainmentSystem : SkipperFile internal class NintendoEntertainmentSystem : Detector
{ {
public NintendoEntertainmentSystem() public NintendoEntertainmentSystem()
{ {
// Create tests // Create tests
var rule1Test1 = new DataSkipperTest var rule1Test1 = new DataTest
{ {
Offset = 0x00, Offset = "0",
Value = new byte[] { 0x4E, 0x45, 0x53, 0x1A }, Value = "4E45531A",
Result = true, Result = true,
}; };
// Create rules // Create rules
var rule1 = new SkipperRule var rule1 = new Rule
{ {
StartOffset = 0x10, StartOffset = "10",
EndOffset = null, EndOffset = "EOF",
Operation = HeaderSkipOperation.None, Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest> Tests = new Test[]
{ {
rule1Test1, rule1Test1,
} }
@@ -35,7 +35,7 @@ namespace SabreTools.Skippers.SkipperFiles
Author = "Roman Scherzer"; Author = "Roman Scherzer";
Version = "1.1"; Version = "1.1";
SourceFile = "nes"; SourceFile = "nes";
Rules = new List<SkipperRule> Rules = new Rule[]
{ {
rule1, rule1,
}; };

View File

@@ -0,0 +1,93 @@
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.Detectors
{
/// <summary>
/// Detector for Nintendo Famicom Disk System headers
/// </summary>
/// <remarks>Originally from fds.xml</remarks>
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,
};
}
}
}

View File

@@ -1,30 +1,30 @@
using System.Collections.Generic; using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.SkipperFiles namespace SabreTools.Skippers.Detectors
{ {
/// <summary> /// <summary>
/// SkipperFile for Super Famicom SPC headers /// Detector for Super Famicom SPC headers
/// </summary> /// </summary>
/// <remarks>Originally from spc.xml</remarks> /// <remarks>Originally from spc.xml</remarks>
internal class SuperFamicomSPC : SkipperFile internal class SuperFamicomSPC : Detector
{ {
public SuperFamicomSPC() public SuperFamicomSPC()
{ {
// Create tests // Create tests
var rule1Test1 = new DataSkipperTest var rule1Test1 = new DataTest
{ {
Offset = 0x00, Offset = "0",
Value = new byte[] { 0x53, 0x4E, 0x45, 0x53, 0x2D, 0x53, 0x50, 0x43 }, Value = "534E45532D535043",
Result = true, Result = true,
}; };
// Create rules // Create rules
var rule1 = new SkipperRule var rule1 = new Rule
{ {
StartOffset = 0x100, StartOffset = "00100",
EndOffset = null, EndOffset = "EOF",
Operation = HeaderSkipOperation.None, Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest> Tests = new Test[]
{ {
rule1Test1, rule1Test1,
} }
@@ -35,7 +35,7 @@ namespace SabreTools.Skippers.SkipperFiles
Author = "Yori Yoshizuki"; Author = "Yori Yoshizuki";
Version = "1.0"; Version = "1.0";
SourceFile = "spc"; SourceFile = "spc";
Rules = new List<SkipperRule> Rules = new Rule[]
{ {
rule1, rule1,
}; };

View File

@@ -0,0 +1,76 @@
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.Detectors
{
/// <summary>
/// Detector for Super Nintendo Entertainment System headers
/// </summary>
/// <remarks>Originally from snes.xml</remarks>
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
};
}
}
}

View File

@@ -7,6 +7,7 @@ namespace SabreTools.Skippers
/// </summary> /// </summary>
public enum HeaderSkipOperation public enum HeaderSkipOperation
{ {
[XmlEnum("none")]
None = 0, None = 0,
[XmlEnum("bitswap")] [XmlEnum("bitswap")]

View File

@@ -1,27 +1,49 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Xml.Serialization; using System.Xml.Serialization;
using SabreTools.Logging; using SabreTools.Logging;
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers namespace SabreTools.Skippers
{ {
public class SkipperRule [XmlType("rule")]
public class Rule
{ {
#region Fields #region Fields
/// <summary> /// <summary>
/// Starting offset for applying rule /// Starting offset for applying rule
/// </summary> /// </summary>
/// <remarks>Either numeric or the literal "EOF"</remarks>
[XmlAttribute("start_offset")] [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);
}
}
/// <summary> /// <summary>
/// Ending offset for applying rule /// Ending offset for applying rule
/// </summary> /// </summary>
/// <remarks>Either numeric or the literal "EOF"</remarks>
[XmlAttribute("end_offset")] [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);
}
}
/// <summary> /// <summary>
/// Byte manipulation operation /// Byte manipulation operation
@@ -32,19 +54,34 @@ namespace SabreTools.Skippers
/// <summary> /// <summary>
/// List of matching tests in a rule /// List of matching tests in a rule
/// </summary> /// </summary>
[XmlArray] [XmlElement("and", typeof(AndTest))]
[XmlArrayItem("data")] [XmlElement("data", typeof(DataTest))]
[XmlArrayItem("or")] [XmlElement("file", typeof(FileTest))]
[XmlArrayItem("xor")] [XmlElement("or", typeof(OrTest))]
[XmlArrayItem("and")] [XmlElement("xor", typeof(XorTest))]
[XmlArrayItem("file")] public Test[]? Tests { get; set; }
public List<SkipperTest> Tests { get; set; }
/// <summary> /// <summary>
/// Filename the skipper rule lives in /// Filename the skipper rule lives in
/// </summary> /// </summary>
[XmlIgnore] [XmlIgnore]
public string SourceFile { get; set; } public string? SourceFile { get; set; }
#endregion
#region Private instance variables
/// <summary>
/// Starting offset for applying rule
/// </summary>
/// <remarks>null is EOF</remarks>
private long? _startOffset = null;
/// <summary>
/// Ending offset for applying rule
/// </summary>
/// <remarks>null is EOF</remarks>
private long? _endOffset = null;
#endregion #endregion
@@ -57,27 +94,25 @@ namespace SabreTools.Skippers
#endregion #endregion
#region Constructors public Rule()
/// <summary>
/// Constructor
/// </summary>
public SkipperRule()
{ {
logger = new Logger(this); logger = new Logger(this);
} }
#endregion
/// <summary> /// <summary>
/// Check if a Stream passes all tests in the SkipperRule /// Check if a Stream passes all tests in the Rule
/// </summary> /// </summary>
/// <param name="input">Stream to check</param> /// <param name="input">Stream to check</param>
/// <returns>True if all tests passed, false otherwise</returns> /// <returns>True if all tests passed, false otherwise</returns>
public bool PassesAllTests(Stream input) public bool PassesAllTests(Stream input)
{ {
bool success = true; 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); bool result = test.Passes(input);
success &= result; success &= result;
@@ -94,16 +129,23 @@ namespace SabreTools.Skippers
/// <returns>True if the file was transformed properly, false otherwise</returns> /// <returns>True if the file was transformed properly, false otherwise</returns>
public bool TransformFile(string input, string output) public bool TransformFile(string input, string output)
{ {
// If the input file doesn't exist, fail // If the input file doesn't exist
if (!File.Exists(input)) 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; return false;
} }
// Create the output directory if it doesn't already // Create the output directory if it doesn't already
if (!Directory.Exists(Path.GetDirectoryName(output))) string parentDirectory = Path.GetDirectoryName(output) ?? string.Empty;
Directory.CreateDirectory(Path.GetDirectoryName(output)); Directory.CreateDirectory(parentDirectory);
//logger.User($"Attempting to apply rule to '{input}'"); //logger.User($"Attempting to apply rule to '{input}'");
bool success = TransformStream(File.OpenRead(input), File.Create(output)); bool success = TransformStream(File.OpenRead(input), File.Create(output));
@@ -134,15 +176,15 @@ namespace SabreTools.Skippers
long extsize = input.Length; long extsize = input.Length;
if ((Operation > HeaderSkipOperation.Bitswap && (extsize % 2) != 0) if ((Operation > HeaderSkipOperation.Bitswap && (extsize % 2) != 0)
|| (Operation > HeaderSkipOperation.Byteswap && (extsize % 4) != 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!"); logger.Error("The stream did not have the correct size to be transformed!");
return false; return false;
} }
// Now read the proper part of the file and apply the rule // Now read the proper part of the file and apply the rule
BinaryWriter bw = null; BinaryWriter? bw = null;
BinaryReader br = null; BinaryReader? br = null;
try try
{ {
logger.User("Applying found rule to input stream"); logger.User("Applying found rule to input stream");
@@ -150,24 +192,24 @@ namespace SabreTools.Skippers
br = new BinaryReader(input); br = new BinaryReader(input);
// Seek to the beginning offset // Seek to the beginning offset
if (StartOffset == null) if (_startOffset == null)
success = false; success = false;
else if (Math.Abs((long)StartOffset) > input.Length) else if (Math.Abs((long)_startOffset) > input.Length)
success = false; success = false;
else if (StartOffset > 0) else if (_startOffset > 0)
input.Seek((long)StartOffset, SeekOrigin.Begin); input.Seek((long)_startOffset, SeekOrigin.Begin);
else if (StartOffset < 0) else if (_startOffset < 0)
input.Seek((long)StartOffset, SeekOrigin.End); input.Seek((long)_startOffset, SeekOrigin.End);
// Then read and apply the operation as you go // Then read and apply the operation as you go
if (success) if (success)
{ {
byte[] buffer = new byte[4]; byte[] buffer = new byte[4];
int pos = 0; int pos = 0;
while (input.Position < (EndOffset ?? input.Length) while (input.Position < (_endOffset ?? input.Length)
&& input.Position < input.Length) && input.Position < input.Length)
{ {
byte b = br.ReadByte(); byte b = br.ReadByte();

View File

@@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks> <TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -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
{
/// <remarks>
/// 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.
/// </remarks>
public class SkipperFile
{
#region Fields
/// <summary>
/// Skipper name
/// </summary>
[XmlElement("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Author names
/// </summary>
[XmlElement("author")]
public string Author { get; set; } = string.Empty;
/// <summary>
/// File version
/// </summary>
[XmlElement("version")]
public string Version { get; set; } = string.Empty;
/// <summary>
/// Set of all rules in the skipper
/// </summary>
[XmlArray("rule")]
public List<SkipperRule> Rules { get; set; } = new List<SkipperRule>();
/// <summary>
/// Filename the skipper lives in
/// </summary>
[XmlIgnore]
public string SourceFile { get; set; } = string.Empty;
#endregion
#region Constructors
/// <summary>
/// Create an empty SkipperFile object
/// </summary>
public SkipperFile() { }
/// <summary>
/// Create a SkipperFile object parsed from an input file
/// </summary>
/// <param name="filename">Name of the file to parse</param>
public SkipperFile(string filename)
{
Rules = new List<SkipperRule>();
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
/// <summary>
/// Parse an XML document in as a SkipperFile
/// </summary>
/// <param name="xtr">XmlReader representing the document</param>
/// <returns>True if the file could be parsed, false otherwise</returns>
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;
}
}
/// <summary>
/// Parse an XML document in as a SkipperRule
/// </summary>
/// <param name="xtr">XmlReader representing the document</param>
/// <returns>Filled SkipperRule on success, null otherwise</returns>
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<SkipperTest>(),
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;
}
}
/// <summary>
/// Parse an XML document in as a SkipperTest
/// </summary>
/// <param name="xtr">XmlReader representing the document</param>
/// <returns>Filled SkipperTest on success, null otherwise</returns>
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<byte>();
test.Result = true;
test.Mask = Array.Empty<byte>();
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
/// <summary>
/// Get the SkipperRule associated with a given stream
/// </summary>
/// <param name="input">Stream to be checked</param>
/// <param name="skipperName">Name of the skipper to be used, blank to find a matching skipper</param>
/// <returns>The SkipperRule that matched the stream, null otherwise</returns>
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;
}
/// <summary>
/// Get the matching SkipperRule from all Rules, if possible
/// </summary>
/// <param name="input">Stream to be checked</param>
/// <returns>The SkipperRule that matched the stream, null otherwise</returns>
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
}
}

View File

@@ -1,63 +0,0 @@
using System.Collections.Generic;
namespace SabreTools.Skippers.SkipperFiles
{
/// <summary>
/// SkipperFile for Atari 7800 headers
/// </summary>
/// <remarks>Originally from a7800.xml</remarks>
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<SkipperTest>
{
rule1Test1,
}
};
var rule2 = new SkipperRule
{
StartOffset = 0x80,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule2Test1,
}
};
// Create file
Name = "Atari 7800";
Author = "Roman Scherzer";
Version = "1.0";
SourceFile = "a7800";
Rules = new List<SkipperRule>
{
rule1,
rule2,
};
}
}
}

View File

@@ -1,120 +0,0 @@
using System.Collections.Generic;
namespace SabreTools.Skippers.SkipperFiles
{
/// <summary>
/// SkipperFile for Commodore PSID headers
/// </summary>
/// <remarks>Originally from psid.xml</remarks>
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<SkipperTest>
{
rule1Test1,
}
};
var rule2 = new SkipperRule
{
StartOffset = 0x76,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule2Test1,
}
};
var rule3 = new SkipperRule
{
StartOffset = 0x7c,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule3Test1,
}
};
var rule4 = new SkipperRule
{
StartOffset = 0x7c,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule4Test1,
}
};
var rule5 = new SkipperRule
{
StartOffset = 0x7c,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule5Test1,
}
};
// Create file
Name = "psid";
Author = "Yori Yoshizuki";
Version = "1.2";
SourceFile = "psid";
Rules = new List<SkipperRule>
{
rule1,
rule2,
rule3,
rule4,
rule5,
};
}
}
}

View File

@@ -1,101 +0,0 @@
using System.Collections.Generic;
namespace SabreTools.Skippers.SkipperFiles
{
/// <summary>
/// SkipperFile for Nintendo Famicom Disk System headers
/// </summary>
/// <remarks>Originally from fds.xml</remarks>
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<SkipperTest>
{
rule1Test1,
}
};
var rule2 = new SkipperRule
{
StartOffset = 0x10,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule2Test1,
}
};
var rule3 = new SkipperRule
{
StartOffset = 0x10,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule3Test1,
}
};
var rule4 = new SkipperRule
{
StartOffset = 0x10,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule4Test1,
}
};
// Create file
Name = "fds";
Author = "Yori Yoshizuki";
Version = "1.0";
SourceFile = "fds";
Rules = new List<SkipperRule>
{
rule1,
rule2,
rule3,
rule4,
};
}
}
}

View File

@@ -1,82 +0,0 @@
using System.Collections.Generic;
namespace SabreTools.Skippers.SkipperFiles
{
/// <summary>
/// SkipperFile for Super Nintendo Entertainment System headers
/// </summary>
/// <remarks>Originally from snes.xml</remarks>
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<SkipperTest>
{
rule1Test1,
}
};
var rule2 = new SkipperRule
{
StartOffset = 0x200,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule2Test1,
}
};
var rule3 = new SkipperRule
{
StartOffset = 0x200,
EndOffset = null,
Operation = HeaderSkipOperation.None,
Tests = new List<SkipperTest>
{
rule3Test1,
}
};
// Create file
Name = "Nintendo Super Famicom/SNES";
Author = "Matt Nadareski (darksabre76)";
Version = "1.0";
SourceFile = "snes";
Rules = new List<SkipperRule>
{
rule1, // FIG
rule2, // SMC
rule3, // UFO
};
}
}
}

View File

@@ -1,5 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using SabreTools.IO; using SabreTools.IO;
using SabreTools.Logging; using SabreTools.Logging;
@@ -23,9 +26,9 @@ namespace SabreTools.Skippers
public static class SkipperMatch public static class SkipperMatch
{ {
/// <summary> /// <summary>
/// Header skippers represented by a list of skipper objects /// Header detectors represented by a list of detector objects
/// </summary> /// </summary>
private static List<SkipperFile> Skippers = null; private static List<Detector>? Skippers = null;
/// <summary> /// <summary>
/// Local paths /// Local paths
@@ -37,7 +40,7 @@ namespace SabreTools.Skippers
/// <summary> /// <summary>
/// Logging object /// Logging object
/// </summary> /// </summary>
private static readonly Logger logger = new Logger(); private static readonly Logger logger = new();
#endregion #endregion
@@ -70,13 +73,43 @@ namespace SabreTools.Skippers
private static void PopulateSkippers() private static void PopulateSkippers()
{ {
// Ensure the list exists // Ensure the list exists
if (Skippers == null) Skippers ??= new List<Detector>();
Skippers = new List<SkipperFile>();
// Create the XML serializer
var xts = new XmlSerializer(typeof(Detector));
// Get skippers for each known header type // 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() private static void PopulateSkippersInternal()
{ {
// Ensure the list exists // Ensure the list exists
if (Skippers == null) Skippers ??= new List<Detector>();
Skippers = new List<SkipperFile>();
// Get skippers for each known header type // Get skippers for each known header type
Skippers.Add(new SkipperFiles.Atari7800()); Skippers.Add(new Detectors.Atari7800());
Skippers.Add(new SkipperFiles.AtariLynx()); Skippers.Add(new Detectors.AtariLynx());
Skippers.Add(new SkipperFiles.CommodorePSID()); Skippers.Add(new Detectors.CommodorePSID());
Skippers.Add(new SkipperFiles.NECPCEngine()); Skippers.Add(new Detectors.NECPCEngine());
Skippers.Add(new SkipperFiles.Nintendo64()); Skippers.Add(new Detectors.Nintendo64());
Skippers.Add(new SkipperFiles.NintendoEntertainmentSystem()); Skippers.Add(new Detectors.NintendoEntertainmentSystem());
Skippers.Add(new SkipperFiles.NintendoFamicomDiskSystem()); Skippers.Add(new Detectors.NintendoFamicomDiskSystem());
Skippers.Add(new SkipperFiles.SuperNintendoEntertainmentSystem()); Skippers.Add(new Detectors.SuperNintendoEntertainmentSystem());
Skippers.Add(new SkipperFiles.SuperFamicomSPC()); Skippers.Add(new Detectors.SuperFamicomSPC());
} }
/// <summary> /// <summary>
/// Get the SkipperRule associated with a given file /// Get the Rule associated with a given file
/// </summary> /// </summary>
/// <param name="input">Name of the file to be checked</param> /// <param name="input">Name of the file to be checked</param>
/// <param name="skipperName">Name of the skipper to be used, blank to find a matching skipper</param> /// <param name="skipperName">Name of the skipper to be used, blank to find a matching skipper</param>
/// <param name="logger">Logger object for file and console output</param> /// <param name="logger">Logger object for file and console output</param>
/// <returns>The SkipperRule that matched the file</returns> /// <returns>The Rule that matched the file</returns>
public static SkipperRule GetMatchingRule(string input, string skipperName) public static Rule GetMatchingRule(string input, string skipperName)
{ {
// If the file doesn't exist, return a blank skipper rule // If the file doesn't exist, return a blank skipper rule
if (!File.Exists(input)) if (!File.Exists(input))
{ {
logger.Error($"The file '{input}' does not exist so it cannot be tested"); 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); return GetMatchingRule(File.OpenRead(input), skipperName);
} }
/// <summary> /// <summary>
/// Get the SkipperRule associated with a given stream /// Get the Rule associated with a given stream
/// </summary> /// </summary>
/// <param name="input">Name of the file to be checked</param> /// <param name="input">Name of the file to be checked</param>
/// <param name="skipperName">Name of the skipper to be used, blank to find a matching skipper</param> /// <param name="skipperName">Name of the skipper to be used, blank to find a matching skipper</param>
/// <param name="keepOpen">True if the underlying stream should be kept open, false otherwise</param> /// <param name="keepOpen">True if the underlying stream should be kept open, false otherwise</param>
/// <returns>The SkipperRule that matched the file</returns> /// <returns>The Rule that matched the file</returns>
public static SkipperRule GetMatchingRule(Stream input, string skipperName, bool keepOpen = false) 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 we have an invalid set of skippers or skipper name
if (skipperName == null) if (Skippers == null || skipperName == null)
return skipperRule; return skipperRule;
// Loop through and find a Skipper that has the right name // Loop through and find a Skipper that has the right name
logger.Verbose("Beginning search for matching header skip rules"); logger.Verbose("Beginning search for matching header skip rules");
List<SkipperFile> tempList = new List<SkipperFile>();
tempList.AddRange(Skippers);
// Loop through all known SkipperFiles // Loop through all known Detectors
foreach (SkipperFile skipper in tempList) foreach (Detector? skipper in Skippers)
{ {
// This should not happen
if (skipper == null)
continue;
skipperRule = skipper.GetMatchingRule(input, skipperName); skipperRule = skipper.GetMatchingRule(input, skipperName);
if (skipperRule != null) if (skipperRule != null)
break; break;
@@ -156,9 +190,8 @@ namespace SabreTools.Skippers
if (!keepOpen) if (!keepOpen)
input.Dispose(); input.Dispose();
// If the SkipperRule is null, make it empty // If the Rule is null, make it empty
if (skipperRule == null) skipperRule ??= new Rule();
skipperRule = new SkipperRule();
// If we have a blank rule, inform the user // If we have a blank rule, inform the user
if (skipperRule.Tests == null) if (skipperRule.Tests == null)

View File

@@ -1,303 +0,0 @@
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace SabreTools.Skippers
{
/// <summary>
/// Individual test that applies to a SkipperRule
/// </summary>
public abstract class SkipperTest
{
#region Fields
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>null is EOF</remarks>
[XmlAttribute("offset")]
public long? Offset { get; set; }
/// <summary>
/// Static value to be checked at the offset
/// </summary>
[XmlAttribute("value")]
public byte[] Value { get; set; }
/// <summary>
/// Determines whether a pass or failure is expected
/// </summary>
[XmlAttribute("result")]
public bool Result { get; set; }
/// <summary>
/// Byte mask to be applied to the tested bytes
/// </summary>
[XmlAttribute("mask")]
public byte[] Mask { get; set; }
/// <summary>
/// Expected size of the input byte array, used with the Operator
/// </summary>
[XmlAttribute("size")]
public long? Size { get; set; } // null is PO2, "power of 2" filesize
/// <summary>
/// Expected range value for the input byte array size, used with Size
/// </summary>
[XmlAttribute("operator")]
public HeaderSkipTestFileOperator Operator { get; set; }
#endregion
/// <summary>
/// Check if a stream passes the test
/// </summary>
/// <param name="input">Stream to check rule against</param>
/// <remarks>The Stream is assumed to be in the proper position for a given test</remarks>
public abstract bool Passes(Stream input);
#region Checking Helpers
/// <summary>
/// Seek an input stream based on the test value
/// </summary>
/// <param name="input">Stream to seek</param>
/// <returns>True if the stream could seek, false on error</returns>
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
}
/// <summary>
/// Skipper test using AND
/// </summary>
[XmlType("and")]
public class AndSkipperTest : SkipperTest
{
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Skipper test using DATA
/// </summary>
[XmlType("data")]
public class DataSkipperTest : SkipperTest
{
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Skipper test using FILE
/// </summary>
[XmlType("file")]
public class FileSkipperTest : SkipperTest
{
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Skipper test using OR
/// </summary>
[XmlType("or")]
public class OrSkipperTest : SkipperTest
{
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Skipper test using XOR
/// </summary>
[XmlType("xor")]
public class XorSkipperTest : SkipperTest
{
/// <inheritdoc/>
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;
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Globalization;
using System.IO;
namespace SabreTools.Skippers
{
/// <summary>
/// Individual test that applies to a Rule
/// </summary>
public abstract class Test
{
/// <summary>
/// Check if a stream passes the test
/// </summary>
/// <param name="input">Stream to check rule against</param>
/// <remarks>The Stream is assumed to be in the proper position for a given test</remarks>
public abstract bool Passes(Stream input);
#region Helpers
/// <summary>
/// Prase a hex string into a byte array
/// </summary>
/// <see href="http://stackoverflow.com/questions/321370/how-can-i-convert-a-hex-string-to-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;
}
/// <summary>
/// Seek an input stream based on the test value
/// </summary>
/// <param name="input">Stream to seek</param>
/// <param name="offset">Offset to seek to</param>
/// <returns>True if the stream could seek, false on error</returns>
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
}
}

View File

@@ -0,0 +1,130 @@
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace SabreTools.Skippers.Tests
{
/// <summary>
/// Test that uses a byte mask AND against data
/// </summary>
[XmlType("and")]
public class AndTest : Test
{
#region Fields
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>Either numeric or the literal "EOF"</remarks>
[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);
}
}
/// <summary>
/// Static value to be checked at the offset
/// </summary>
/// <remarks>Hex string representation of a byte array</remarks>
[XmlAttribute("value")]
public string? Value
{
get => _value == null ? string.Empty : BitConverter.ToString(_value).Replace("-", string.Empty);
set => _value = ParseByteArrayFromHex(value);
}
/// <summary>
/// Determines whether a pass or failure is expected
/// </summary>
[XmlAttribute("result")]
public bool Result { get; set; } = true;
/// <summary>
/// Byte mask to be applied to the tested bytes
/// </summary>
/// <remarks>Hex string representation of a byte array</remarks>
[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
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>null is EOF</remarks>
private long? _offset;
/// <summary>
/// Static value to be checked at the offset
/// </summary>
private byte[]? _value;
/// <summary>
/// Byte mask to be applied to the tested bytes
/// </summary>
private byte[]? _mask;
#endregion
/// <inheritdoc/>
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;
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace SabreTools.Skippers.Tests
{
/// <summary>
/// Test that checks data matches
/// </summary>
[XmlType("data")]
public class DataTest : Test
{
#region Fields
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>Either numeric or the literal "EOF"</remarks>
[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);
}
}
/// <summary>
/// Static value to be checked at the offset
/// </summary>
/// <remarks>Hex string representation of a byte array</remarks>
[XmlAttribute("value")]
public string? Value
{
get => _value == null ? string.Empty : BitConverter.ToString(_value).Replace("-", string.Empty);
set => _value = ParseByteArrayFromHex(value);
}
/// <summary>
/// Determines whether a pass or failure is expected
/// </summary>
[XmlAttribute("result")]
public bool Result { get; set; } = true;
#endregion
#region Private instance variables
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>null is EOF</remarks>
private long? _offset;
/// <summary>
/// Static value to be checked at the offset
/// </summary>
private byte[]? _value;
#endregion
/// <inheritdoc/>
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;
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace SabreTools.Skippers.Tests
{
/// <summary>
/// Test that tests file size
/// </summary>
[XmlType("file")]
public class FileTest : Test
{
#region Fields
/// <summary>
/// Determines whether a pass or failure is expected
/// </summary>
[XmlAttribute("result")]
public bool Result { get; set; } = true;
/// <summary>
/// Expected size of the input byte array, used with the Operator
/// </summary>
/// <remarks>Either numeric or the literal "po2"</remarks>
[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);
}
}
/// <summary>
/// Expected range value for the input byte array size, used with Size
/// </summary>
[XmlAttribute("operator")]
public HeaderSkipTestFileOperator Operator { get; set; }
#endregion
#region Private instance variables
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>null is PO2 ("power of 2" filesize)</remarks>
private long? _size;
#endregion
/// <inheritdoc/>
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;
}
}
}

View File

@@ -0,0 +1,130 @@
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace SabreTools.Skippers.Tests
{
/// <summary>
/// Test that uses a byte mask OR against data
/// </summary>
[XmlType("or")]
public class OrTest : Test
{
#region Fields
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>Either numeric or the literal "EOF"</remarks>
[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);
}
}
/// <summary>
/// Static value to be checked at the offset
/// </summary>
/// <remarks>Hex string representation of a byte array</remarks>
[XmlAttribute("value")]
public string? Value
{
get => _value == null ? string.Empty : BitConverter.ToString(_value).Replace("-", string.Empty);
set => _value = ParseByteArrayFromHex(value);
}
/// <summary>
/// Determines whether a pass or failure is expected
/// </summary>
[XmlAttribute("result")]
public bool Result { get; set; } = true;
/// <summary>
/// Byte mask to be applied to the tested bytes
/// </summary>
/// <remarks>Hex string representation of a byte array</remarks>
[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
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>null is EOF</remarks>
private long? _offset;
/// <summary>
/// Static value to be checked at the offset
/// </summary>
private byte[]? _value;
/// <summary>
/// Byte mask to be applied to the tested bytes
/// </summary>
private byte[]? _mask;
#endregion
/// <inheritdoc/>
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;
}
}
}

View File

@@ -0,0 +1,130 @@
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace SabreTools.Skippers.Tests
{
/// <summary>
/// Test that uses a byte mask XOR against data
/// </summary>
[XmlType("xor")]
public class XorTest : Test
{
#region Fields
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>Either numeric or the literal "EOF"</remarks>
[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);
}
}
/// <summary>
/// Static value to be checked at the offset
/// </summary>
/// <remarks>Hex string representation of a byte array</remarks>
[XmlAttribute("value")]
public string? Value
{
get => _value == null ? string.Empty : BitConverter.ToString(_value).Replace("-", string.Empty);
set => _value = ParseByteArrayFromHex(value);
}
/// <summary>
/// Determines whether a pass or failure is expected
/// </summary>
[XmlAttribute("result")]
public bool Result { get; set; } = true;
/// <summary>
/// Byte mask to be applied to the tested bytes
/// </summary>
/// <remarks>Hex string representation of a byte array</remarks>
[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
/// <summary>
/// File offset to run the test
/// </summary>
/// <remarks>null is EOF</remarks>
private long? _offset;
/// <summary>
/// Static value to be checked at the offset
/// </summary>
private byte[]? _value;
/// <summary>
/// Byte mask to be applied to the tested bytes
/// </summary>
private byte[]? _mask;
#endregion
/// <inheritdoc/>
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;
}
}
}

View File

@@ -6,9 +6,9 @@ using Xunit;
namespace SabreTools.Test.Skippers namespace SabreTools.Test.Skippers
{ {
[Collection("SkipperMatch")] [Collection("SkipperMatch")]
public class SkipperRuleTransformTests public class RuleTransformTests
{ {
public SkipperRuleTransformTests() public RuleTransformTests()
{ {
SkipperMatch.Init(false); SkipperMatch.Init(false);
} }
@@ -231,7 +231,7 @@ namespace SabreTools.Test.Skippers
/// <summary> /// <summary>
/// Pad the stream to 1KiB then seek to beginning /// Pad the stream to 1KiB then seek to beginning
/// </summary> /// </summary>
private void PadAndReset(MemoryStream ms) private static void PadAndReset(MemoryStream ms)
{ {
for (long i = ms.Length; i < 1024; i++) for (long i = ms.Length; i < 1024; i++)
{ {

View File

@@ -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 // Get the skipper rule that matches the file, if any
SkipperMatch.Init(); 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 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; return false;
logger.User("File has a valid copier header"); 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 // Extract the header as a string for the database
using var fs = File.OpenRead(file); using var fs = File.OpenRead(file);
byte[] hbin = new byte[(int)rule.StartOffset]; byte[] hbin = new byte[int.Parse(rule.StartOffset)];
fs.Read(hbin, 0, (int)rule.StartOffset); fs.Read(hbin, 0, int.Parse(rule.StartOffset));
hstr = Utilities.ByteArrayToString(hbin); hstr = Utilities.ByteArrayToString(hbin);
} }
catch catch