using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; namespace SabreTools.Skippers { /// /// It is well worth considering just moving the XML files to code, similar to how RV does it /// if only because nobody really has any skippers outside of this. It would also make the /// output directory cleaner and less prone to user error in case something didn't get copied /// correctly. The contents of these files should still be added to the wiki, in that case. /// public class SkipperFile { #region Fields /// /// Skipper name /// [XmlElement("name")] public string Name { get; set; } = string.Empty; /// /// Author names /// [XmlElement("author")] public string Author { get; set; } = string.Empty; /// /// File version /// [XmlElement("version")] public string Version { get; set; } = string.Empty; /// /// Set of all rules in the skipper /// [XmlArray("rule")] public List Rules { get; set; } = new List(); /// /// Filename the skipper lives in /// [XmlIgnore] public string SourceFile { get; set; } = string.Empty; #endregion #region Constructors /// /// Create an empty SkipperFile object /// public SkipperFile() { } /// /// Create a SkipperFile object parsed from an input file /// /// Name of the file to parse public SkipperFile(string filename) { Rules = new List(); SourceFile = Path.GetFileNameWithoutExtension(filename); XmlReader xtr = XmlReader.Create(filename, new XmlReaderSettings { CheckCharacters = false, DtdProcessing = DtdProcessing.Ignore, IgnoreComments = true, IgnoreWhitespace = true, ValidationFlags = XmlSchemaValidationFlags.None, ValidationType = ValidationType.None, }); bool valid = Parse(xtr); // If we somehow have an invalid file, zero out the fields if (!valid) { Name = null; Author = null; Version = null; Rules = null; SourceFile = null; } } #endregion #region Parsing Helpers /// /// Parse an XML document in as a SkipperFile /// /// XmlReader representing the document /// True if the file could be parsed, false otherwise private bool Parse(XmlReader xtr) { if (xtr == null) return false; try { bool valid = false; xtr.MoveToContent(); while (!xtr.EOF) { if (xtr.NodeType != XmlNodeType.Element) xtr.Read(); switch (xtr.Name.ToLowerInvariant()) { case "detector": valid = true; xtr.Read(); break; case "name": Name = xtr.ReadElementContentAsString(); break; case "author": Author = xtr.ReadElementContentAsString(); break; case "version": Version = xtr.ReadElementContentAsString(); break; case "rule": SkipperRule rule = ParseRule(xtr); if (rule != null) Rules.Add(rule); xtr.Read(); break; default: xtr.Read(); break; } } return valid; } catch { return false; } } /// /// Parse an XML document in as a SkipperRule /// /// XmlReader representing the document /// Filled SkipperRule on success, null otherwise private SkipperRule ParseRule(XmlReader xtr) { if (xtr == null) return null; try { // Get the information from the rule first SkipperRule rule = new SkipperRule { StartOffset = null, EndOffset = null, Operation = HeaderSkipOperation.None, Tests = new List(), SourceFile = this.SourceFile, }; string startOffset = xtr.GetAttribute("start_offset"); if (startOffset != null) { if (startOffset.ToLowerInvariant() == "eof") rule.StartOffset = null; else rule.StartOffset = Convert.ToInt64(startOffset, 16); } string endOffset = xtr.GetAttribute("end_offset"); if (endOffset != null) { if (endOffset.ToLowerInvariant() == "eof") rule.EndOffset = null; else rule.EndOffset = Convert.ToInt64(endOffset, 16); } string operation = xtr.GetAttribute("operation"); if (operation != null) { switch (operation.ToLowerInvariant()) { case "bitswap": rule.Operation = HeaderSkipOperation.Bitswap; break; case "byteswap": rule.Operation = HeaderSkipOperation.Byteswap; break; case "wordswap": rule.Operation = HeaderSkipOperation.Wordswap; break; case "wordbyteswap": rule.Operation = HeaderSkipOperation.WordByteswap; break; } } // Now read the individual tests into the Rule XmlReader subreader = xtr.ReadSubtree(); if (subreader != null) { subreader.MoveToContent(); while (!subreader.EOF) { if (subreader.NodeType != XmlNodeType.Element) subreader.Read(); switch (xtr.Name.ToLowerInvariant()) { case "data": case "or": case "xor": case "and": case "file": SkipperTest test = ParseTest(subreader); if (test != null) rule.Tests.Add(test); subreader.Read(); break; default: subreader.Read(); break; } } } return rule; } catch { return null; } } /// /// Parse an XML document in as a SkipperTest /// /// XmlReader representing the document /// Filled SkipperTest on success, null otherwise private SkipperTest ParseTest(XmlReader xtr) { if (xtr == null) return null; try { // Get the test type SkipperTest test = xtr.Name.ToLowerInvariant() switch { "and" => new AndSkipperTest(), "data" => new DataSkipperTest(), "file" => new FileSkipperTest(), "or" => new OrSkipperTest(), "xor" => new XorSkipperTest(), _ => null, }; // If we had an invalid test type if (test == null) return null; // Set the default values test.Offset = 0; test.Value = Array.Empty(); test.Result = true; test.Mask = Array.Empty(); test.Size = 0; test.Operator = HeaderSkipTestFileOperator.Equal; // Now populate all the parts that we can if (xtr.GetAttribute("offset") != null) { string offset = xtr.GetAttribute("offset"); if (offset.ToLowerInvariant() == "eof") test.Offset = null; else test.Offset = Convert.ToInt64(offset, 16); } if (xtr.GetAttribute("value") != null) { string value = xtr.GetAttribute("value"); // http://stackoverflow.com/questions/321370/how-can-i-convert-a-hex-string-to-a-byte-array test.Value = new byte[value.Length / 2]; for (int index = 0; index < test.Value.Length; index++) { string byteValue = value.Substring(index * 2, 2); test.Value[index] = byte.Parse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture); } } if (xtr.GetAttribute("result") != null) { string result = xtr.GetAttribute("result"); if (!bool.TryParse(result, out bool resultBool)) resultBool = true; test.Result = resultBool; } if (xtr.GetAttribute("mask") != null) { string mask = xtr.GetAttribute("mask"); // http://stackoverflow.com/questions/321370/how-can-i-convert-a-hex-string-to-a-byte-array test.Mask = new byte[mask.Length / 2]; for (int index = 0; index < test.Mask.Length; index++) { string byteValue = mask.Substring(index * 2, 2); test.Mask[index] = byte.Parse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture); } } if (xtr.GetAttribute("size") != null) { string size = xtr.GetAttribute("size"); if (size.ToLowerInvariant() == "po2") test.Size = null; else test.Size = Convert.ToInt64(size, 16); } if (xtr.GetAttribute("operator") != null) { string oper = xtr.GetAttribute("operator"); test.Operator = oper.ToLowerInvariant() switch { "less" => HeaderSkipTestFileOperator.Less, "greater" => HeaderSkipTestFileOperator.Greater, "equal" => HeaderSkipTestFileOperator.Equal, _ => HeaderSkipTestFileOperator.Equal, }; } return test; } catch { return null; } } #endregion #region Matching /// /// Get the SkipperRule associated with a given stream /// /// Stream to be checked /// Name of the skipper to be used, blank to find a matching skipper /// The SkipperRule that matched the stream, null otherwise public SkipperRule GetMatchingRule(Stream input, string skipperName) { // If we have no name supplied, try to blindly match if (string.IsNullOrWhiteSpace(skipperName)) return GetMatchingRule(input); // If the name matches the internal name of the skipper else if (string.Equals(skipperName, Name, StringComparison.OrdinalIgnoreCase)) return GetMatchingRule(input); // If the name matches the source file name of the skipper else if (string.Equals(skipperName, SourceFile, StringComparison.OrdinalIgnoreCase)) return GetMatchingRule(input); // Otherwise, nothing matches by default return null; } /// /// Get the matching SkipperRule from all Rules, if possible /// /// Stream to be checked /// The SkipperRule that matched the stream, null otherwise private SkipperRule GetMatchingRule(Stream input) { // Loop through the rules until one is found that works foreach (SkipperRule rule in Rules) { // Always reset the stream back to the original place input.Seek(0, SeekOrigin.Begin); // If all tests in the rule pass, we return this rule if (rule.PassesAllTests(input)) return rule; } // If nothing passed, we return null by default return null; } #endregion } }