using System; using System.Collections.Generic; using System.IO; namespace SabreTools.Library.Skippers { public class SkipperRule { #region Fields /// /// Starting offset for applying rule /// public long? StartOffset { get; set; } // null is EOF /// /// Ending offset for applying rule /// public long? EndOffset { get; set; } // null if EOF /// /// Byte manipulation operation /// public HeaderSkipOperation Operation { get; set; } /// /// List of matching tests in a rule /// public List Tests { get; set; } /// /// Filename the skipper rule lives in /// public string SourceFile { get; set; } #endregion #region Logging /// /// Logging object /// //private readonly Logger logger; // TODO: Re-enable all logging once Logging namespace separated out #endregion #region Constructors /// /// Constructor /// public SkipperRule() { //logger = new Logger(this); } #endregion /// /// Check if a Stream passes all tests in the SkipperRule /// /// Stream to check /// True if all tests passed, false otherwise public bool PassesAllTests(Stream input) { bool success = true; foreach (SkipperTest 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, fail if (!File.Exists(input)) { //logger.Error($"I'm sorry but '{input}' doesn't exist!"); return false; } // Create the output directory if it doesn't already Ensure(Path.GetDirectoryName(output)); //logger.User($"Attempting to apply rule to '{input}'"); bool success = TransformStream(TryOpenRead(input), TryCreate(output)); // If the output file has size 0, delete it if (new FileInfo(output).Length == 0) { TryDelete(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; } if (pos % 2 == 0) { 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; } // TODO: Remove this region once IO namespace is separated out properly #region TEMPORARY - REMOVEME /// /// Ensure the output directory is a proper format and can be created /// /// Directory to check /// True if the directory should be created, false otherwise (default) /// True if this is a temp directory, false otherwise /// Full path to the directory public static string Ensure(string dir, bool create = false, bool temp = false) { // If the output directory is invalid if (string.IsNullOrWhiteSpace(dir)) { if (temp) dir = Path.GetTempPath(); else dir = Environment.CurrentDirectory; } // Get the full path for the output directory dir = Path.GetFullPath(dir); // If we're creating the output folder, do so if (create) Directory.CreateDirectory(dir); return dir; } /// /// Try to create a file for write, optionally throwing the error /// /// Name of the file to create /// True if the error that is thrown should be thrown back to the caller, false otherwise /// An opened stream representing the file on success, null otherwise public static FileStream TryCreate(string file, bool throwOnError = false) { // Now wrap opening the file try { return File.Open(file, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); } catch (Exception ex) { if (throwOnError) throw ex; else return null; } } /// /// Try to safely delete a file, optionally throwing the error /// /// Name of the file to delete /// True if the error that is thrown should be thrown back to the caller, false otherwise /// True if the file didn't exist or could be deleted, false otherwise public static bool TryDelete(string file, bool throwOnError = false) { // Check if the file exists first if (!File.Exists(file)) return true; // Now wrap deleting the file try { File.Delete(file); return true; } catch (Exception ex) { if (throwOnError) throw ex; else return false; } } /// /// Try to open a file for read, optionally throwing the error /// /// Name of the file to open /// True if the error that is thrown should be thrown back to the caller, false otherwise /// An opened stream representing the file on success, null otherwise public static FileStream TryOpenRead(string file, bool throwOnError = false) { // Check if the file exists first if (!File.Exists(file)) return null; // Now wrap opening the file try { return File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); } catch (Exception ex) { if (throwOnError) throw ex; else return null; } } #endregion } }