using System; using System.IO; using System.Xml.Serialization; using SabreTools.Logging; using SabreTools.Skippers.Tests; namespace SabreTools.Skippers { [XmlType("rule")] public class Rule { #region Fields /// /// Starting offset for applying rule /// /// Either numeric or the literal "EOF" [XmlAttribute("start_offset")] public string? StartOffset { get => _startOffset == null ? "EOF" : _startOffset.Value.ToString(); set { if (value == null || value.ToLowerInvariant() == "eof") _startOffset = null; else _startOffset = Convert.ToInt64(value, fromBase: 16); } } /// /// Ending offset for applying rule /// /// Either numeric or the literal "EOF" [XmlAttribute("end_offset")] public string? EndOffset { get => _endOffset == null ? "EOF" : _endOffset.Value.ToString(); set { if (value == null || value.ToLowerInvariant() == "eof") _endOffset = null; else _endOffset = Convert.ToInt64(value, fromBase: 16); } } /// /// Byte manipulation operation /// [XmlAttribute("operation")] public HeaderSkipOperation Operation { get; set; } /// /// List of matching tests in a rule /// [XmlElement("and", typeof(AndTest))] [XmlElement("data", typeof(DataTest))] [XmlElement("file", typeof(FileTest))] [XmlElement("or", typeof(OrTest))] [XmlElement("xor", typeof(XorTest))] public Test[]? Tests { get; set; } /// /// Filename the skipper rule lives in /// [XmlIgnore] public string? SourceFile { get; set; } #endregion #region Private instance variables /// /// Starting offset for applying rule /// /// null is EOF private long? _startOffset = null; /// /// Ending offset for applying rule /// /// null is EOF private long? _endOffset = null; #endregion #region Logging /// /// Logging object /// private readonly Logger logger; #endregion public Rule() { logger = new Logger(this); } /// /// Check if a Stream passes all tests in the Rule /// /// Stream to check /// True if all tests passed, false otherwise public bool PassesAllTests(Stream input) { bool success = true; // 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; } /// /// Transform an input file using the given rule /// /// Input file name /// Output file name /// True if the file was transformed properly, false otherwise public bool TransformFile(string input, string output) { // If the input file doesn't exist if (string.IsNullOrWhiteSpace(input) || !File.Exists(input)) { logger.Error($"'{input}' doesn't exist and cannot be transformed!"); return false; } // If we have an invalid output directory name if (string.IsNullOrWhiteSpace(output)) { logger.Error($"Output path was null or empty, cannot write transformed file!"); return false; } // Create the output directory if it doesn't already 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; } /// /// Transform an input stream using the given rule /// /// Input stream /// Output stream /// True if the underlying read stream should be kept open, false otherwise /// True if the underlying write stream should be kept open, false otherwise /// True if the file was transformed properly, false otherwise public bool TransformStream(Stream input, Stream output, bool keepReadOpen = false, bool keepWriteOpen = false) { bool success = true; // 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))) { logger.Error("The stream did not have the correct size to be transformed!"); return false; } // Now read the proper part of the file and apply the rule BinaryWriter? bw = null; BinaryReader? br = null; try { logger.User("Applying found rule to input stream"); 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 (Exception ex) { logger.Error(ex); return false; } finally { // 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(); } return success; } } }