using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using SabreTools.Library.Data; using SabreTools.Library.DatFiles; using SabreTools.Library.FileTypes; using SabreTools.Library.Tools; using Compress.ThreadReaders; namespace SabreTools.Library.IO { /// /// Extensions to Stream functionality /// public static class StreamExtensions { /// /// 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 public static void AppendBytes(Stream input, Stream output, byte[] bytesToAddToHead, byte[] bytesToAddToTail) { // Write out prepended bytes if (bytesToAddToHead != null && bytesToAddToHead.Count() > 0) output.Write(bytesToAddToHead, 0, bytesToAddToHead.Length); // Now copy the existing file over input.CopyTo(output); // Write out appended bytes if (bytesToAddToTail != null && bytesToAddToTail.Count() > 0) output.Write(bytesToAddToTail, 0, bytesToAddToTail.Length); } /// /// Retrieve file information for a single file /// /// Filename to get information from /// Size of the input stream /// Hash flag saying what hashes should not be calculated (defaults to none) /// True if the underlying read stream should be kept open, false otherwise /// Populated BaseFile object if success, empty one on error public static BaseFile GetInfo(this Stream input, long size = -1, bool keepReadOpen = false) { return GetInfoAsync(input, size, keepReadOpen).ConfigureAwait(false).GetAwaiter().GetResult(); } /// /// Retrieve file information for a single file /// /// Filename to get information from /// Size of the input stream /// True if the underlying read stream should be kept open, false otherwise /// Populated BaseFile object if success, empty one on error public static async Task GetInfoAsync(Stream input, long size = -1, bool keepReadOpen = false) { // If we want to automatically set the size if (size == -1) size = input.Length; try { // Get a list of hashers to run over the buffer List hashers = new List() { new Hasher(Hash.CRC), new Hasher(Hash.MD5), #if NET_FRAMEWORK new Hasher(Hash.RIPEMD160), #endif new Hasher(Hash.SHA1), new Hasher(Hash.SHA256), new Hasher(Hash.SHA384), new Hasher(Hash.SHA512), new Hasher(Hash.SpamSum), }; // Initialize the hashing helpers var loadBuffer = new ThreadLoadBuffer(input); int buffersize = 3 * 1024 * 1024; byte[] buffer0 = new byte[buffersize]; byte[] buffer1 = new byte[buffersize]; /* Please note that some of the following code is adapted from RomVault. This is a modified version of how RomVault does threaded hashing. As such, some of the terminology and code is the same, though variable names and comments may have been tweaked to better fit this code base. */ // Pre load the first buffer long refsize = size; int next = refsize > buffersize ? buffersize : (int)refsize; input.Read(buffer0, 0, next); int current = next; refsize -= next; bool bufferSelect = true; while (current > 0) { // Trigger the buffer load on the second buffer next = refsize > buffersize ? buffersize : (int)refsize; if (next > 0) loadBuffer.Trigger(bufferSelect ? buffer1 : buffer0, next); byte[] buffer = bufferSelect ? buffer0 : buffer1; // Run hashes in parallel await Task.WhenAll(hashers.Select(h => h.Process(buffer, current))); // Wait for the load buffer worker, if needed if (next > 0) loadBuffer.Wait(); // Setup for the next hashing step current = next; refsize -= next; bufferSelect = !bufferSelect; } // Finalize all hashing helpers loadBuffer.Finish(); await Task.WhenAll(hashers.Select(h => h.Finalize())); // Get the results BaseFile baseFile = new BaseFile() { Size = size, CRC = hashers.First(h => h.HashType == Hash.CRC).GetHash(), MD5 = hashers.First(h => h.HashType == Hash.MD5).GetHash(), #if NET_FRAMEWORK RIPEMD160 = hashers.First(h => h.HashType == Hash.RIPEMD160).GetHash(), #endif SHA1 = hashers.First(h => h.HashType == Hash.SHA1).GetHash(), SHA256 = hashers.First(h => h.HashType == Hash.SHA256).GetHash(), SHA384 = hashers.First(h => h.HashType == Hash.SHA384).GetHash(), SHA512 = hashers.First(h => h.HashType == Hash.SHA512).GetHash(), SpamSum = hashers.First(h => h.HashType == Hash.SpamSum).GetHash(), }; // Dispose of the hashers loadBuffer.Dispose(); hashers.ForEach(h => h.Dispose()); return baseFile; } catch (IOException ex) { Globals.Logger.Warning(ex, "An exception occurred during hashing."); return new BaseFile(); } finally { if (!keepReadOpen) input.Dispose(); else input.SeekIfPossible(); } } /// /// Seek to a specific point in the stream, if possible /// /// Input stream to try seeking on /// Optional offset to seek to public static long SeekIfPossible(this Stream input, long offset = 0) { try { if (input.CanSeek) { if (offset < 0) return input.Seek(offset, SeekOrigin.End); else if (offset >= 0) return input.Seek(offset, SeekOrigin.Begin); } return input.Position; } catch (NotSupportedException ex) { Globals.Logger.Verbose(ex, "Stream does not support seeking to starting offset. Stream position not changed"); } catch (NotImplementedException ex) { Globals.Logger.Warning(ex, "Stream does not support seeking to starting offset. Stream position not changed"); } return -1; } } }