Use Nuget package for Skippers

This commit is contained in:
Matt Nadareski
2024-02-29 16:09:24 -05:00
parent db99ccaba3
commit 2b25ab167e
24 changed files with 3 additions and 1595 deletions

View File

@@ -29,11 +29,11 @@
<ProjectReference Include="..\SabreTools.Filtering\SabreTools.Filtering.csproj" />
<ProjectReference Include="..\SabreTools.Logging\SabreTools.Logging.csproj" />
<ProjectReference Include="..\SabreTools.Reports\SabreTools.Reports.csproj" />
<ProjectReference Include="..\SabreTools.Skippers\SabreTools.Skippers.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.IO" Version="1.3.0" />
<PackageReference Include="SabreTools.Skippers" Version="1.1.2" />
</ItemGroup>
</Project>

View File

@@ -24,7 +24,6 @@
<ProjectReference Include="..\NaturalSort\NaturalSort.csproj" />
<ProjectReference Include="..\SabreTools.Core\SabreTools.Core.csproj" />
<ProjectReference Include="..\SabreTools.Logging\SabreTools.Logging.csproj" />
<ProjectReference Include="..\SabreTools.Skippers\SabreTools.Skippers.csproj" />
</ItemGroup>
<!-- Support for old .NET versions -->
@@ -36,6 +35,7 @@
<ItemGroup>
<PackageReference Include="SabreTools.IO" Version="1.3.0" />
<PackageReference Include="SabreTools.Skippers" Version="1.1.2" />
</ItemGroup>
</Project>

View File

@@ -1,99 +0,0 @@
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace SabreTools.Skippers
{
[XmlRoot("detector")]
public abstract class Detector
{
#region Fields
/// <summary>
/// Detector name
/// </summary>
[XmlElement("name")]
public string? Name { get; protected set; }
/// <summary>
/// Author names
/// </summary>
[XmlElement("author")]
public string? Author { get; protected set; }
/// <summary>
/// File version
/// </summary>
[XmlElement("version")]
public string? Version { get; protected set; }
/// <summary>
/// Set of all rules in the skipper
/// </summary>
[XmlElement("rule")]
public Rule[]? Rules { get; protected set; }
/// <summary>
/// Filename the skipper lives in
/// </summary>
[XmlIgnore]
public string? SourceFile { get; protected 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.IsNullOrEmpty(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

@@ -1,33 +0,0 @@
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("1", "415441524937383030", true);
var rule2Test1 = new DataTest("64", "41435455414C20434152542044415441205354415254532048455245", true);
// Create rules
var rule1 = new Rule("80", "EOF", HeaderSkipOperation.None, [rule1Test1], "a7800");
var rule2 = new Rule("80", "EOF", HeaderSkipOperation.None, [rule2Test1], "a7800");
// Create file
Name = "Atari 7800";
Author = "Roman Scherzer";
Version = "1.0";
SourceFile = "a7800";
Rules =
[
rule1,
rule2,
];
}
}
}

View File

@@ -1,33 +0,0 @@
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.Detectors
{
/// <summary>
/// Detector for Atari Lynx headers
/// </summary>
/// <remarks>Originally from lynx.xml</remarks>
internal class AtariLynx : Detector
{
public AtariLynx()
{
// Create tests
var rule1Test1 = new DataTest("0", "4C594E58", true);
var rule2Test1 = new DataTest("6", "425339", true);
// Create rules
var rule1 = new Rule("40", "EOF", HeaderSkipOperation.None, [rule1Test1], "lynx");
var rule2 = new Rule("40", "EOF", HeaderSkipOperation.None, [rule2Test1], "lynx");
// Create file
Name = "Atari Lynx";
Author = "Roman Scherzer";
Version = "1.0";
SourceFile = "lynx";
Rules =
[
rule1,
rule2,
];
}
}
}

View File

@@ -1,42 +0,0 @@
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("0", "5053494400010076", true);
var rule2Test1 = new DataTest("0", "505349440003007c", true);
var rule3Test1 = new DataTest("0", "505349440002007c", true);
var rule4Test1 = new DataTest("0", "505349440001007c", true);
var rule5Test1 = new DataTest("0", "525349440002007c", true);
// Create rules
var rule1 = new Rule("76", "EOF", HeaderSkipOperation.None, [rule1Test1], "psid");
var rule2 = new Rule("76", "EOF", HeaderSkipOperation.None, [rule2Test1], "psid");
var rule3 = new Rule("7c", "EOF", HeaderSkipOperation.None, [rule3Test1], "psid");
var rule4 = new Rule("7c", "EOF", HeaderSkipOperation.None, [rule4Test1], "psid");
var rule5 = new Rule("7c", "EOF", HeaderSkipOperation.None, [rule5Test1], "psid");
// Create file
Name = "psid";
Author = "Yori Yoshizuki";
Version = "1.2";
SourceFile = "psid";
Rules =
[
rule1,
rule2,
rule3,
rule4,
rule5,
];
}
}
}

View File

@@ -1,30 +0,0 @@
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.Detectors
{
/// <summary>
/// Detector for NEC PC-Engine / TurboGrafx 16 headers
/// </summary>
/// <remarks>Originally from pce.xml</remarks>
internal class NECPCEngine : Detector
{
public NECPCEngine()
{
// Create tests
var rule1Test1 = new DataTest("0", "4000000000000000AABB02", true);
// Create rules
var rule1 = new Rule("200", null, HeaderSkipOperation.None, [rule1Test1], "pce");
// Create file
Name = "NEC TurboGrafx-16/PC-Engine";
Author = "Matt Nadareski (darksabre76)";
Version = "1.0";
SourceFile = "pce";
Rules =
[
rule1,
];
}
}
}

View File

@@ -1,36 +0,0 @@
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.Detectors
{
/// <summary>
/// Detector for Nintendo 64 headers
/// </summary>
/// <remarks>Originally from n64.xml</remarks>
internal class Nintendo64 : Detector
{
public Nintendo64()
{
// Create tests
var v64Test = new DataTest("0", "80371240", true);
var z64Test = new DataTest("0", "37804012", true);
var n64Test = new DataTest("0", "40123780", true);
// Create rules
var v64Rule = new Rule("0", "EOF", HeaderSkipOperation.None, [v64Test], "n64");
var z64Rule = new Rule("0", "EOF", HeaderSkipOperation.Byteswap, [z64Test], "n64");
var n64Rule = new Rule("0", "EOF", HeaderSkipOperation.Wordswap, [n64Test], "n64");
// Create file
Name = "Nintendo 64 - ABCD";
Author = "CUE";
Version = "1.1";
SourceFile = "n64";
Rules =
[
v64Rule,
z64Rule,
n64Rule,
];
}
}
}

View File

@@ -1,30 +0,0 @@
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.Detectors
{
/// <summary>
/// Detector for Nintendo Entertainment System headers
/// </summary>
/// <remarks>Originally from nes.xml</remarks>
internal class NintendoEntertainmentSystem : Detector
{
public NintendoEntertainmentSystem()
{
// Create tests
var inesTest = new DataTest("0", "4E45531A", true);
// Create rules
var inesRule = new Rule("10", "EOF", HeaderSkipOperation.None, [inesTest], "nes");
// Create file
Name = "Nintendo Famicon/NES";
Author = "Roman Scherzer";
Version = "1.1";
SourceFile = "nes";
Rules =
[
inesRule,
];
}
}
}

View File

@@ -1,39 +0,0 @@
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("0", "4644531A010000000000000000000000", true);
var rule2Test1 = new DataTest("0", "4644531A020000000000000000000000", true);
var rule3Test1 = new DataTest("0", "4644531A030000000000000000000000", true);
var rule4Test1 = new DataTest("0", "4644531A040000000000000000000000", true);
// Create rules
var rule1 = new Rule("10", null, HeaderSkipOperation.None, [rule1Test1], "fds");
var rule2 = new Rule("10", null, HeaderSkipOperation.None, [rule2Test1], "fds");
var rule3 = new Rule("10", null, HeaderSkipOperation.None, [rule3Test1], "fds");
var rule4 = new Rule("10", null, HeaderSkipOperation.None, [rule4Test1], "fds");
// Create file
Name = "fds";
Author = "Yori Yoshizuki";
Version = "1.0";
SourceFile = "fds";
Rules =
[
rule1,
rule2,
rule3,
rule4,
];
}
}
}

View File

@@ -1,30 +0,0 @@
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers.Detectors
{
/// <summary>
/// Detector for Super Famicom SPC headers
/// </summary>
/// <remarks>Originally from spc.xml</remarks>
internal class SuperFamicomSPC : Detector
{
public SuperFamicomSPC()
{
// Create tests
var rule1Test1 = new DataTest("0", "534E45532D535043", true);
// Create rules
var rule1 = new Rule("00100", "EOF", HeaderSkipOperation.None, [rule1Test1], "spc");
// Create file
Name = "Nintendo Super Famicon SPC";
Author = "Yori Yoshizuki";
Version = "1.0";
SourceFile = "spc";
Rules =
[
rule1,
];
}
}
}

View File

@@ -1,36 +0,0 @@
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 figTest = new DataTest("16", "0000000000000000", true);
var smcTest = new DataTest("16", "AABB040000000000", true);
var ufoTest = new DataTest("16", "535550455255464F", true);
// Create rules
var figRule = new Rule("200", null, HeaderSkipOperation.None, [figTest], "snes");
var smcRule = new Rule("200", null, HeaderSkipOperation.None, [smcTest], "snes");
var ufoRule = new Rule("200", null, HeaderSkipOperation.None, [ufoTest], "snes");
// Create file
Name = "Nintendo Super Famicom/SNES";
Author = "Matt Nadareski (darksabre76)";
Version = "1.0";
SourceFile = "snes";
Rules =
[
figRule,
smcRule,
ufoRule,
];
}
}
}

View File

@@ -1,40 +0,0 @@
using System.Xml.Serialization;
namespace SabreTools.Skippers
{
/// <summary>
/// Determines the header skip operation
/// </summary>
public enum HeaderSkipOperation
{
[XmlEnum("none")]
None = 0,
[XmlEnum("bitswap")]
Bitswap,
[XmlEnum("byteswap")]
Byteswap,
[XmlEnum("wordswap")]
Wordswap,
[XmlEnum("wordbyteswap")]
WordByteswap,
}
/// <summary>
/// Determines the operator to be used in a file test
/// </summary>
public enum HeaderSkipTestFileOperator
{
[XmlEnum("equal")]
Equal = 0,
[XmlEnum("less")]
Less,
[XmlEnum("greater")]
Greater,
}
}

View File

@@ -1,286 +0,0 @@
using System;
using System.IO;
using System.Xml.Serialization;
using SabreTools.Skippers.Tests;
namespace SabreTools.Skippers
{
[XmlType("rule")]
public class Rule
{
#region Fields
/// <summary>
/// Starting offset for applying rule
/// </summary>
/// <remarks>Either numeric or the literal "EOF"</remarks>
[XmlAttribute("start_offset")]
public string? StartOffset
{
get => _startOffset == null ? "EOF" : _startOffset.Value.ToString();
set
{
if (value == null || value.Equals("eof", StringComparison.InvariantCultureIgnoreCase))
_startOffset = null;
else
_startOffset = Convert.ToInt64(value, fromBase: 16);
}
}
/// <summary>
/// Ending offset for applying rule
/// </summary>
/// <remarks>Either numeric or the literal "EOF"</remarks>
[XmlAttribute("end_offset")]
public string? EndOffset
{
get => _endOffset == null ? "EOF" : _endOffset.Value.ToString();
set
{
if (value == null || value.Equals("eof", StringComparison.InvariantCultureIgnoreCase))
_endOffset = null;
else
_endOffset = Convert.ToInt64(value, fromBase: 16);
}
}
/// <summary>
/// Byte manipulation operation
/// </summary>
[XmlAttribute("operation")]
public HeaderSkipOperation Operation { get; set; }
/// <summary>
/// List of matching tests in a rule
/// </summary>
[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; }
/// <summary>
/// Filename the skipper rule lives in
/// </summary>
[XmlIgnore]
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
/// <summary>
/// Constructor
/// </summary>
public Rule(string? startOffset, string? endOffset, HeaderSkipOperation operation, Test[]? tests, string? sourceFile)
{
StartOffset = startOffset;
EndOffset = endOffset;
Operation = operation;
Tests = tests;
SourceFile = sourceFile;
}
/// <summary>
/// Check if a Stream passes all tests in the Rule
/// </summary>
/// <param name="input">Stream to check</param>
/// <returns>True if all tests passed, false otherwise</returns>
public bool PassesAllTests(Stream input)
{
bool success = true;
// 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;
}
return success;
}
/// <summary>
/// Transform an input file using the given rule
/// </summary>
/// <param name="input">Input file name</param>
/// <param name="output">Output file name</param>
/// <returns>True if the file was transformed properly, false otherwise</returns>
public bool TransformFile(string input, string output)
{
// If the input file doesn't exist
if (string.IsNullOrEmpty(input) || !File.Exists(input))
return false;
// If we have an invalid output directory name
if (string.IsNullOrEmpty(output))
return false;
// Create the output directory if it doesn't already
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));
// If the output file has size 0, delete it
if (new FileInfo(output).Length == 0)
{
File.Delete(output);
success = false;
}
return success;
}
/// <summary>
/// Transform an input stream using the given rule
/// </summary>
/// <param name="input">Input stream</param>
/// <param name="output">Output stream</param>
/// <param name="keepReadOpen">True if the underlying read stream should be kept open, false otherwise</param>
/// <param name="keepWriteOpen">True if the underlying write stream should be kept open, false otherwise</param>
/// <returns>True if the file was transformed properly, false otherwise</returns>
public bool TransformStream(Stream? input, Stream output, bool keepReadOpen = false, bool keepWriteOpen = false)
{
bool success = true;
// If the input stream isn't valid
if (input == null || !input.CanRead)
return false;
// If the sizes are wrong for the values, fail
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)))
{
return false;
}
// Now read the proper part of the file and apply the rule
BinaryWriter? bw = null;
BinaryReader? br = null;
try
{
bw = new BinaryWriter(output);
br = new BinaryReader(input);
// Seek to the beginning offset
if (_startOffset == null)
success = false;
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.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)
&& input.Position < input.Length)
{
byte b = br.ReadByte();
switch (Operation)
{
case HeaderSkipOperation.Bitswap:
// http://stackoverflow.com/questions/3587826/is-there-a-built-in-function-to-reverse-bit-order
uint r = b;
int s = 7;
for (b >>= 1; b != 0; b >>= 1)
{
r <<= 1;
r |= (byte)(b & 1);
s--;
}
r <<= s;
buffer[pos] = (byte)r;
break;
case HeaderSkipOperation.Byteswap:
if (pos % 2 == 1)
buffer[pos - 1] = b;
else
buffer[pos + 1] = b;
break;
case HeaderSkipOperation.Wordswap:
buffer[3 - pos] = b;
break;
case HeaderSkipOperation.WordByteswap:
buffer[(pos + 2) % 4] = b;
break;
case HeaderSkipOperation.None:
default:
buffer[pos] = b;
break;
}
// Set the buffer position to default write to
pos = (pos + 1) % 4;
// If we filled a buffer, flush to the stream
if (pos == 0)
{
bw.Write(buffer);
bw.Flush();
buffer = new byte[4];
}
}
// If there's anything more in the buffer, write only the left bits
for (int i = 0; i < pos; i++)
{
bw.Write(buffer[i]);
}
}
}
catch
{
return false;
}
finally
{
#if NET40_OR_GREATER
// If we're not keeping the read stream open, dispose of the binary reader
if (!keepReadOpen)
br?.Dispose();
// If we're not keeping the write stream open, dispose of the binary reader
if (!keepWriteOpen)
bw?.Dispose();
#endif
}
return success;
}
}
}

View File

@@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.1.2</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Copyright>Copyright (c)2016-2024 Matt Nadareski</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<RepositoryUrl>https://github.com/SabreTools/SabreTools</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
</Project>

View File

@@ -1,121 +0,0 @@
using System.Collections.Generic;
using System.IO;
namespace SabreTools.Skippers
{
/// <summary>
/// Class for matching existing Skippers
/// <summary>
/// <remarks>
/// Skippers, in general, are distributed as XML files by some projects
/// in order to denote a way of transforming a file so that it will match
/// the hashes included in their DATs. Each skipper file can contain multiple
/// skipper rules, each of which denote a type of header/transformation. In
/// turn, each of those rules can contain multiple tests that denote that
/// a file should be processed using that rule. Transformations can include
/// simply skipping over a portion of the file all the way to byteswapping
/// the entire file. For the purposes of this library, Skippers also denote
/// a way of changing files directly in order to produce a file whose external
/// hash would match those same DATs.
/// </remarks>
public static class SkipperMatch
{
/// <summary>
/// Header detectors represented by a list of detector objects
/// </summary>
private static List<Detector>? Skippers = null;
/// <summary>
/// Initialize static fields
/// </summary>
public static void Init()
{
// If the list is populated, don't add to it
if (Skippers != null)
return;
// Generate header skippers internally
PopulateSkippers();
}
/// <summary>
/// Populate the entire list of header skippers from generated objects
/// </summary>
/// <remarks>
/// http://mamedev.emulab.it/clrmamepro/docs/xmlheaders.txt
/// http://www.emulab.it/forum/index.php?topic=127.0
/// </remarks>
private static void PopulateSkippers()
{
// Ensure the list exists
Skippers ??= [];
// Get skippers for each known header type
Skippers.Add(new Detectors.Atari7800());
Skippers.Add(new Detectors.AtariLynx());
Skippers.Add(new Detectors.CommodorePSID());
Skippers.Add(new Detectors.NECPCEngine());
Skippers.Add(new Detectors.Nintendo64());
Skippers.Add(new Detectors.NintendoEntertainmentSystem());
Skippers.Add(new Detectors.NintendoFamicomDiskSystem());
Skippers.Add(new Detectors.SuperNintendoEntertainmentSystem());
Skippers.Add(new Detectors.SuperFamicomSPC());
}
/// <summary>
/// Get the Rule associated with a given file
/// </summary>
/// <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="logger">Logger object for file and console output</param>
/// <returns>The Rule that matched the file</returns>
public static Rule GetMatchingRule(string input, string skipperName)
{
// If the file doesn't exist, return a blank skipper rule
if (!File.Exists(input))
return new Rule(null, null, HeaderSkipOperation.None, null, null);
return GetMatchingRule(File.OpenRead(input), skipperName);
}
/// <summary>
/// Get the Rule associated with a given stream
/// </summary>
/// <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="keepOpen">True if the underlying stream should be kept open, false otherwise</param>
/// <returns>The Rule that matched the file</returns>
public static Rule GetMatchingRule(Stream? input, string skipperName, bool keepOpen = false)
{
var skipperRule = new Rule(null, null, HeaderSkipOperation.None, null, null);
// If we have an invalid input
if (input == null || !input.CanRead)
return skipperRule;
// If we have an invalid set of skippers or skipper name
if (Skippers == null || skipperName == null)
return skipperRule;
// Loop through all known Detectors
foreach (Detector? skipper in Skippers)
{
// This should not happen
if (skipper == null)
continue;
skipperRule = skipper.GetMatchingRule(input, skipperName);
if (skipperRule != null)
break;
}
// If we're not keeping the stream open, dispose of the binary reader
if (!keepOpen)
input?.Dispose();
// If the Rule is null, make it empty
skipperRule ??= new Rule(null, null, HeaderSkipOperation.None, null, null);
return skipperRule;
}
}
}

View File

@@ -1,73 +0,0 @@
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.IsNullOrEmpty(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

@@ -1,141 +0,0 @@
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
/// <summary>
/// Constructor
/// </summary>
public AndTest(string? offset, string? value, bool result, string? mask)
{
Offset = offset;
Value = value;
Result = result;
Mask = mask;
}
/// <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

@@ -1,111 +0,0 @@
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
/// <summary>
/// Constructor
/// </summary>
public DataTest(string? offset, string? value, bool result)
{
Offset = offset;
Value = value;
Result = result;
}
/// <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

@@ -1,97 +0,0 @@
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
/// <summary>
/// Constructor
/// </summary>
public FileTest(bool result, string? size, HeaderSkipTestFileOperator opr)
{
Result = result;
Size = size;
Operator = opr;
}
/// <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

@@ -1,141 +0,0 @@
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
/// <summary>
/// Constructor
/// </summary>
public OrTest(string? offset, string? value, bool result, string? mask)
{
Offset = offset;
Value = value;
Result = result;
Mask = mask;
}
/// <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

@@ -1,141 +0,0 @@
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
/// <summary>
/// Constructor
/// </summary>
public XorTest(string? offset, string? value, bool result, string? mask)
{
Offset = offset;
Value = value;
Result = result;
Mask = mask;
}
/// <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

@@ -12,7 +12,6 @@
<ProjectReference Include="..\SabreTools.DatTools\SabreTools.DatTools.csproj" />
<ProjectReference Include="..\SabreTools.FileTypes\SabreTools.FileTypes.csproj" />
<ProjectReference Include="..\SabreTools.Filtering\SabreTools.Filtering.csproj" />
<ProjectReference Include="..\SabreTools.Skippers\SabreTools.Skippers.csproj" />
</ItemGroup>
<ItemGroup>
@@ -34,6 +33,7 @@
<PackageReference Include="SabreTools.IO" Version="1.3.0" />
<PackageReference Include="SabreTools.Models" Version="1.3.0" />
<PackageReference Include="SabreTools.Serialization" Version="1.3.0" />
<PackageReference Include="SabreTools.Skippers" Version="1.1.2" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -18,8 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SabreTools.Core", "SabreTools.Core\SabreTools.Core.csproj", "{66E2FB10-77C0-4589-9DCD-3CA48702C18A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Skippers", "SabreTools.Skippers\SabreTools.Skippers.csproj", "{D8665F27-75E6-4E3F-9F0A-286433831C69}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Help", "SabreTools.Help\SabreTools.Help.csproj", "{55364167-844F-4B58-8280-F5327FA3D8E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Logging", "SabreTools.Logging\SabreTools.Logging.csproj", "{3D54D896-19F0-4723-B1E3-E40FAFE5A078}"
@@ -80,14 +78,6 @@ Global
{66E2FB10-77C0-4589-9DCD-3CA48702C18A}.Release|Any CPU.Build.0 = Release|Any CPU
{66E2FB10-77C0-4589-9DCD-3CA48702C18A}.Release|x64.ActiveCfg = Release|Any CPU
{66E2FB10-77C0-4589-9DCD-3CA48702C18A}.Release|x64.Build.0 = Release|Any CPU
{D8665F27-75E6-4E3F-9F0A-286433831C69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D8665F27-75E6-4E3F-9F0A-286433831C69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8665F27-75E6-4E3F-9F0A-286433831C69}.Debug|x64.ActiveCfg = Debug|Any CPU
{D8665F27-75E6-4E3F-9F0A-286433831C69}.Debug|x64.Build.0 = Debug|Any CPU
{D8665F27-75E6-4E3F-9F0A-286433831C69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D8665F27-75E6-4E3F-9F0A-286433831C69}.Release|Any CPU.Build.0 = Release|Any CPU
{D8665F27-75E6-4E3F-9F0A-286433831C69}.Release|x64.ActiveCfg = Release|Any CPU
{D8665F27-75E6-4E3F-9F0A-286433831C69}.Release|x64.Build.0 = Release|Any CPU
{55364167-844F-4B58-8280-F5327FA3D8E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{55364167-844F-4B58-8280-F5327FA3D8E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55364167-844F-4B58-8280-F5327FA3D8E7}.Debug|x64.ActiveCfg = Debug|Any CPU