Port code from SabreTools

This commit is contained in:
Matt Nadareski
2024-02-29 16:01:06 -05:00
parent 7e65202f60
commit c4e3ec209f
24 changed files with 1626 additions and 21 deletions

24
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,24 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "shell",
"args": [
"build",
// Ask dotnet build to generate full paths for file names.
"/property:GenerateFullPaths=true",
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:NoSummary"
],
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
}
]
}

99
Detector.cs Normal file
View File

@@ -0,0 +1,99 @@
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
}
}

33
Detectors/Atari7800.cs Normal file
View File

@@ -0,0 +1,33 @@
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,
];
}
}
}

33
Detectors/AtariLynx.cs Normal file
View File

@@ -0,0 +1,33 @@
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

@@ -0,0 +1,42 @@
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,
];
}
}
}

30
Detectors/NECPCEngine.cs Normal file
View File

@@ -0,0 +1,30 @@
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,
];
}
}
}

36
Detectors/Nintendo64.cs Normal file
View File

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,30 @@
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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,30 @@
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

@@ -0,0 +1,36 @@
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,
];
}
}
}

40
Enums.cs Normal file
View File

@@ -0,0 +1,40 @@
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,
}
}

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 SabreTools
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
README.MD Normal file
View File

@@ -0,0 +1,5 @@
# SabreTools.Skippers
This library comprises of code to perform copier header operations such as matching, manipulation, and removal. These are used for many older console-based systems and helps define known header skippers for immediate use.
Find the link to the Nuget package [here](https://www.nuget.org/packages/SabreTools.Skippers).

286
Rule.cs Normal file
View File

@@ -0,0 +1,286 @@
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

@@ -0,0 +1,30 @@
<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>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.Skippers</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>skippers headers game</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>

View File

@@ -3,6 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Skippers", "SabreTools.Skippers.csproj", "{D71725DB-0DA3-4D92-9ADC-9601D136126C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -11,4 +13,10 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D71725DB-0DA3-4D92-9ADC-9601D136126C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D71725DB-0DA3-4D92-9ADC-9601D136126C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D71725DB-0DA3-4D92-9ADC-9601D136126C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D71725DB-0DA3-4D92-9ADC-9601D136126C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

121
SkipperMatch.cs Normal file
View File

@@ -0,0 +1,121 @@
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;
}
}
}

73
Test.cs Normal file
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.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
}
}

141
Tests/AndTest.cs Normal file
View File

@@ -0,0 +1,141 @@
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;
}
}
}

111
Tests/DataTest.cs Normal file
View File

@@ -0,0 +1,111 @@
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;
}
}
}

97
Tests/FileTest.cs Normal file
View File

@@ -0,0 +1,97 @@
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;
}
}
}

141
Tests/OrTest.cs Normal file
View File

@@ -0,0 +1,141 @@
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;
}
}
}

141
Tests/XorTest.cs Normal file
View File

@@ -0,0 +1,141 @@
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;
}
}
}