using System; using System.Collections.Generic; using System.IO; using SabreTools.Data; using SabreTools.Logging; using SabreTools.Library.FileTypes; using SabreTools.Library.Tools; using SabreTools.Skippers; using Microsoft.Data.Sqlite; namespace SabreTools.Library.IO { /// /// Class for wrapping general file transformations /// /// /// 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. /// public static class Transform { /// /// Header skippers represented by a list of skipper objects /// private static List List; /// /// Local paths /// private static string LocalPath = Path.Combine(Globals.ExeDir, "Skippers") + Path.DirectorySeparatorChar; #region Logging /// /// Logging object /// private static readonly Logger logger = new Logger(); #endregion /// /// Initialize static fields /// public static void Init() { PopulateSkippers(); } /// /// Populate the entire list of header Skippers /// /// /// http://mamedev.emulab.it/clrmamepro/docs/xmlheaders.txt /// http://www.emulab.it/forum/index.php?topic=127.0 /// private static void PopulateSkippers() { if (List == null) List = new List(); foreach (string skipperFile in Directory.EnumerateFiles(LocalPath, "*", SearchOption.AllDirectories)) { List.Add(new SkipperFile(Path.GetFullPath(skipperFile))); } } /// /// Detect header skipper compliance and create an output file /// /// Name of the file to be parsed /// Output directory to write the file to, empty means the same directory as the input file /// True if headers should not be stored in the database, false otherwise /// True if the output file was created, false otherwise public static bool DetectTransformStore(string file, string outDir, bool nostore) { // Create the output directory if it doesn't exist if (!Directory.Exists(outDir)) Directory.CreateDirectory(outDir); logger.User($"\nGetting skipper information for '{file}'"); // Get the skipper rule that matches the file, if any SkipperRule rule = GetMatchingRule(file, string.Empty); // If we have an empty rule, return false if (rule.Tests == null || rule.Tests.Count == 0 || rule.Operation != HeaderSkipOperation.None) return false; logger.User("File has a valid copier header"); // Get the header bytes from the file first string hstr; try { // Extract the header as a string for the database #if NET_FRAMEWORK using (var fs = File.OpenRead(file)) { #else using var fs = File.OpenRead(file); #endif byte[] hbin = new byte[(int)rule.StartOffset]; fs.Read(hbin, 0, (int)rule.StartOffset); hstr = Utilities.ByteArrayToString(hbin); #if NET_FRAMEWORK } #endif } catch { return false; } // Apply the rule to the file string newfile = (string.IsNullOrWhiteSpace(outDir) ? Path.GetFullPath(file) + ".new" : Path.Combine(outDir, Path.GetFileName(file))); rule.TransformFile(file, newfile); // If the output file doesn't exist, return false if (!File.Exists(newfile)) return false; // Now add the information to the database if it's not already there if (!nostore) { BaseFile baseFile = BaseFile.GetInfo(newfile, hashes: Hash.SHA1, asFiles: TreatAsFile.NonArchive); AddHeaderToDatabase(hstr, Utilities.ByteArrayToString(baseFile.SHA1), rule.SourceFile); } return true; } /// /// Detect and replace header(s) to the given file /// /// Name of the file to be parsed /// Output directory to write the file to, empty means the same directory as the input file /// True if a header was found and appended, false otherwise public static bool RestoreHeader(string file, string outDir) { // Create the output directory if it doesn't exist if (!string.IsNullOrWhiteSpace(outDir) && !Directory.Exists(outDir)) Directory.CreateDirectory(outDir); // First, get the SHA-1 hash of the file BaseFile baseFile = BaseFile.GetInfo(file, hashes: Hash.SHA1, asFiles: TreatAsFile.NonArchive); // Retrieve a list of all related headers from the database List headers = RetrieveHeadersFromDatabase(Utilities.ByteArrayToString(baseFile.SHA1)); // If we have nothing retrieved, we return false if (headers.Count == 0) return false; // Now loop through and create the reheadered files, if possible for (int i = 0; i < headers.Count; i++) { string outputFile = (string.IsNullOrWhiteSpace(outDir) ? $"{Path.GetFullPath(file)}.new" : Path.Combine(outDir, Path.GetFileName(file))) + i; logger.User($"Creating reheadered file: {outputFile}"); AppendBytes(file, outputFile, Utilities.StringToByteArray(headers[i]), null); logger.User("Reheadered file created!"); } return true; } /// /// Get the SkipperRule associated with a given file /// /// Name of the file to be checked /// Name of the skipper to be used, blank to find a matching skipper /// Logger object for file and console output /// The SkipperRule that matched the file public static SkipperRule GetMatchingRule(string input, string skipperName) { // If the file doesn't exist, return a blank skipper rule if (!File.Exists(input)) { logger.Error($"The file '{input}' does not exist so it cannot be tested"); return new SkipperRule(); } return GetMatchingRule(File.OpenRead(input), skipperName); } /// /// Get the SkipperRule associated with a given stream /// /// Name of the file to be checked /// Name of the skipper to be used, blank to find a matching skipper /// True if the underlying stream should be kept open, false otherwise /// The SkipperRule that matched the file public static SkipperRule GetMatchingRule(Stream input, string skipperName, bool keepOpen = false) { SkipperRule skipperRule = new SkipperRule(); // If we have a null skipper name, we return since we're not matching skippers if (skipperName == null) return skipperRule; // Loop through and find a Skipper that has the right name logger.Verbose("Beginning search for matching header skip rules"); List tempList = new List(); tempList.AddRange(List); // Loop through all known SkipperFiles foreach (SkipperFile skipper in tempList) { 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 SkipperRule is null, make it empty if (skipperRule == null) skipperRule = new SkipperRule(); // If we have a blank rule, inform the user if (skipperRule.Tests == null) logger.Verbose("No matching rule found!"); else logger.User("Matching rule found!"); return skipperRule; } /// /// Add an aribtrary number of bytes to the inputted file /// /// File to be appended to /// Outputted file /// Bytes to be added to head of file /// Bytes to be added to tail of file private static void AppendBytes(string input, string output, byte[] bytesToAddToHead, byte[] bytesToAddToTail) { // If any of the inputs are invalid, skip if (!File.Exists(input)) return; #if NET_FRAMEWORK using (FileStream fsr = File.OpenRead(input)) using (FileStream fsw = File.OpenWrite(output)) { #else using FileStream fsr = File.OpenRead(input); using FileStream fsw = File.OpenWrite(output); #endif AppendBytes(fsr, fsw, bytesToAddToHead, bytesToAddToTail); #if NET_FRAMEWORK } #endif } /// /// Add an aribtrary number of bytes to the inputted stream /// /// Stream to be appended to /// Outputted stream /// Bytes to be added to head of stream /// Bytes to be added to tail of stream private static void AppendBytes(Stream input, Stream output, byte[] bytesToAddToHead, byte[] bytesToAddToTail) { // Write out prepended bytes if (bytesToAddToHead != null && bytesToAddToHead.Length > 0) output.Write(bytesToAddToHead, 0, bytesToAddToHead.Length); // Now copy the existing file over input.CopyTo(output); // Write out appended bytes if (bytesToAddToTail != null && bytesToAddToTail.Length > 0) output.Write(bytesToAddToTail, 0, bytesToAddToTail.Length); } #region Database Operations /// /// Ensure that the databse exists and has the proper schema /// /// Name of the databse /// Connection string for SQLite private static void EnsureDatabase(string db, string connectionString) { // Make sure the file exists if (!File.Exists(db)) File.Create(db); // Open the database connection SqliteConnection dbc = new SqliteConnection(connectionString); dbc.Open(); // Make sure the database has the correct schema try { string query = @" CREATE TABLE IF NOT EXISTS data ( 'sha1' TEXT NOT NULL, 'header' TEXT NOT NULL, 'type' TEXT NOT NULL, PRIMARY KEY (sha1, header, type) )"; SqliteCommand slc = new SqliteCommand(query, dbc); slc.ExecuteNonQuery(); slc.Dispose(); } catch (Exception ex) { logger.Error(ex); } finally { dbc.Dispose(); } } /// /// Add a header to the database /// /// String representing the header bytes /// SHA-1 of the deheadered file /// Name of the source skipper file private static void AddHeaderToDatabase(string header, string SHA1, string source) { // Ensure the database exists EnsureDatabase(Constants.HeadererFileName, Constants.HeadererConnectionString); // Open the database connection SqliteConnection dbc = new SqliteConnection(Constants.HeadererConnectionString); dbc.Open(); string query = $"SELECT * FROM data WHERE sha1='{SHA1}' AND header='{header}'"; SqliteCommand slc = new SqliteCommand(query, dbc); SqliteDataReader sldr = slc.ExecuteReader(); bool exists = sldr.HasRows; if (!exists) { query = $"INSERT INTO data (sha1, header, type) VALUES ('{SHA1}', '{header}', '{source}')"; slc = new SqliteCommand(query, dbc); logger.Verbose($"Result of inserting header: {slc.ExecuteNonQuery()}"); } // Dispose of database objects slc.Dispose(); sldr.Dispose(); dbc.Dispose(); } /// /// Retrieve headers from the database /// /// SHA-1 of the deheadered file /// List of strings representing the headers to add private static List RetrieveHeadersFromDatabase(string SHA1) { // Ensure the database exists EnsureDatabase(Constants.HeadererFileName, Constants.HeadererConnectionString); // Open the database connection SqliteConnection dbc = new SqliteConnection(Constants.HeadererConnectionString); dbc.Open(); // Create the output list of headers List headers = new List(); string query = $"SELECT header, type FROM data WHERE sha1='{SHA1}'"; SqliteCommand slc = new SqliteCommand(query, dbc); SqliteDataReader sldr = slc.ExecuteReader(); if (sldr.HasRows) { while (sldr.Read()) { logger.Verbose($"Found match with rom type '{sldr.GetString(1)}'"); headers.Add(sldr.GetString(0)); } } else { logger.Warning("No matching header could be found!"); } // Dispose of database objects slc.Dispose(); sldr.Dispose(); dbc.Dispose(); return headers; } #endregion } }