mirror of
https://github.com/claunia/SabreTools.git
synced 2025-12-16 19:14:27 +00:00
IO extensions to IO namespace
This commit is contained in:
@@ -1,174 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Library.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Big endian reading overloads for BinaryReader
|
||||
/// </summary>
|
||||
public static class BinaryReaderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the specified number of bytes from the stream, starting from a specified point in the byte array.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer to read data into.</param>
|
||||
/// <param name="index">The starting point in the buffer at which to begin reading into the buffer.</param>
|
||||
/// <param name="count">The number of bytes to read.</param>
|
||||
/// <returns>The number of bytes read into buffer. This might be less than the number of bytes requested if that many bytes are not available, or it might be zero if the end of the stream is reached.</returns>
|
||||
public static int ReadBigEndian(this BinaryReader reader, byte[] buffer, int index, int count)
|
||||
{
|
||||
int retval = reader.Read(buffer, index, count);
|
||||
Array.Reverse(buffer);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the specified number of characters from the stream, starting from a specified point in the character array.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer to read data into.</param>
|
||||
/// <param name="index">The starting point in the buffer at which to begin reading into the buffer.</param>
|
||||
/// <param name="count">The number of characters to read.</param>
|
||||
/// <returns>The total number of characters read into the buffer. This might be less than the number of characters requested if that many characters are not currently available, or it might be zero if the end of the stream is reached.</returns>
|
||||
public static int ReadBigEndian(this BinaryReader reader, char[] buffer, int index, int count)
|
||||
{
|
||||
int retval = reader.Read(buffer, index, count);
|
||||
Array.Reverse(buffer);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the specified number of bytes from the current stream into a byte array and advances the current position by that number of bytes.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of bytes to read. This value must be 0 or a non-negative number or an exception will occur.</param>
|
||||
/// <returns>A byte array containing data read from the underlying stream. This might be less than the number of bytes requested if the end of the stream is reached.</returns>
|
||||
public static byte[] ReadBytesBigEndian(this BinaryReader reader, int count)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(count);
|
||||
Array.Reverse(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the specified number of characters from the current stream, returns the data in a character array, and advances the current position in accordance with the Encoding used and the specific character being read from the stream.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of characters to read. This value must be 0 or a non-negative number or an exception will occur.</param>
|
||||
/// <returns>A character array containing data read from the underlying stream. This might be less than the number of bytes requested if the end of the stream is reached.</returns>
|
||||
public static char[] ReadCharsBigEndian(this BinaryReader reader, int count)
|
||||
{
|
||||
char[] retval = reader.ReadChars(count);
|
||||
Array.Reverse(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a decimal value from the current stream and advances the current position of the stream by sixteen bytes.
|
||||
/// </summary>
|
||||
/// <returns>A decimal value read from the current stream.</returns>
|
||||
public static decimal ReadDecimalBigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(16);
|
||||
Array.Reverse(retval);
|
||||
|
||||
int i1 = BitConverter.ToInt32(retval, 0);
|
||||
int i2 = BitConverter.ToInt32(retval, 4);
|
||||
int i3 = BitConverter.ToInt32(retval, 8);
|
||||
int i4 = BitConverter.ToInt32(retval, 12);
|
||||
|
||||
return new decimal(new int[] { i1, i2, i3, i4 });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// eads an 8-byte floating point value from the current stream and advances the current position of the stream by eight bytes.
|
||||
/// </summary>
|
||||
/// <returns>An 8-byte floating point value read from the current stream.</returns>
|
||||
public static double ReadDoubleBigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(8);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToDouble(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 2-byte signed integer from the current stream and advances the current position of the stream by two bytes.
|
||||
/// </summary>
|
||||
/// <returns>A 2-byte signed integer read from the current stream.</returns>
|
||||
public static short ReadInt16BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(2);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToInt16(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 4-byte signed integer from the current stream and advances the current position of the stream by four bytes.
|
||||
/// </summary>
|
||||
/// <returns>A 4-byte signed integer read from the current stream.</returns>
|
||||
public static int ReadInt32BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(4);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToInt32(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an 8-byte signed integer from the current stream and advances the current position of the stream by eight bytes.
|
||||
/// </summary>
|
||||
/// <returns>An 8-byte signed integer read from the current stream.</returns>
|
||||
public static long ReadInt64BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(8);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToInt64(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 4-byte floating point value from the current stream and advances the current position of the stream by four bytes.
|
||||
/// </summary>
|
||||
/// <returns>A 4-byte floating point value read from the current stream.</returns>
|
||||
public static float ReadSingleBigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(4);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToSingle(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 2-byte unsigned integer from the current stream using little-endian encoding and advances the position of the stream by two bytes.
|
||||
///
|
||||
/// This API is not CLS-compliant.
|
||||
/// </summary>
|
||||
/// <returns>A 2-byte unsigned integer read from this stream.</returns>
|
||||
public static ushort ReadUInt16BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(2);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToUInt16(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 4-byte unsigned integer from the current stream and advances the position of the stream by four bytes.
|
||||
///
|
||||
/// This API is not CLS-compliant.
|
||||
/// </summary>
|
||||
/// <returns>A 4-byte unsigned integer read from this stream.</returns>
|
||||
public static uint ReadUInt32BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(4);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToUInt32(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an 8-byte unsigned integer from the current stream and advances the position of the stream by eight bytes.
|
||||
///
|
||||
/// This API is not CLS-compliant.
|
||||
/// </summary>
|
||||
/// <returns>An 8-byte unsigned integer read from this stream.</returns>
|
||||
public static ulong ReadUInt64BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(8);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToUInt64(retval, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using SabreTools.Library.Data;
|
||||
using NaturalSort;
|
||||
|
||||
namespace SabreTools.Library.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions to Directory functionality
|
||||
/// </summary>
|
||||
public static class DirectoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Cleans out the temporary directory
|
||||
/// </summary>
|
||||
/// <param name="dir">Name of the directory to clean out</param>
|
||||
public static void Clean(string dir)
|
||||
{
|
||||
foreach (string file in Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
FileExtensions.TryDelete(file);
|
||||
}
|
||||
|
||||
foreach (string subdir in Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
TryDelete(subdir);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure the output directory is a proper format and can be created
|
||||
/// </summary>
|
||||
/// <param name="dir">Directory to check</param>
|
||||
/// <param name="create">True if the directory should be created, false otherwise (default)</param>
|
||||
/// <param name="temp">True if this is a temp directory, false otherwise</param>
|
||||
/// <returns>Full path to the directory</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of just directories from inputs
|
||||
/// </summary>
|
||||
/// <param name="inputs">List of strings representing directories and files</param>
|
||||
/// <param name="appendparent">True if the parent name should be included in the ParentablePath, false otherwise (default)</param>
|
||||
/// <returns>List of strings representing just directories from the inputs</returns>
|
||||
public static List<ParentablePath> GetDirectoriesOnly(List<string> inputs, bool appendparent = false)
|
||||
{
|
||||
List<ParentablePath> outputs = new List<ParentablePath>();
|
||||
foreach (string input in inputs)
|
||||
{
|
||||
if (Directory.Exists(input))
|
||||
{
|
||||
// Get the parent path in case of appending
|
||||
string parentPath = Path.GetFullPath(input);
|
||||
|
||||
List<string> directories = GetDirectoriesOrdered(input);
|
||||
foreach (string dir in directories)
|
||||
{
|
||||
try
|
||||
{
|
||||
outputs.Add(new ParentablePath(Path.GetFullPath(dir), appendparent ? parentPath : string.Empty));
|
||||
}
|
||||
catch (PathTooLongException)
|
||||
{
|
||||
Globals.Logger.Warning($"The path for '{dir}' was too long");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Globals.Logger.Error(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of directories from a directory recursively in proper order
|
||||
/// </summary>
|
||||
/// <param name="dir">Directory to parse</param>
|
||||
/// <returns>List with all new files</returns>
|
||||
private static List<string> GetDirectoriesOrdered(string dir)
|
||||
{
|
||||
return GetDirectoriesOrderedHelper(dir, new List<string>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of directories from a directory recursively in proper order
|
||||
/// </summary>
|
||||
/// <param name="dir">Directory to parse</param>
|
||||
/// <param name="infiles">List representing existing files</param>
|
||||
/// <returns>List with all new files</returns>
|
||||
private static List<string> GetDirectoriesOrderedHelper(string dir, List<string> infiles)
|
||||
{
|
||||
// Take care of the files in the top directory
|
||||
List<string> toadd = Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly).ToList();
|
||||
toadd.Sort(new NaturalComparer());
|
||||
infiles.AddRange(toadd);
|
||||
|
||||
// Then recurse through and add from the directories
|
||||
foreach (string subDir in toadd)
|
||||
{
|
||||
infiles = GetDirectoriesOrderedHelper(subDir, infiles);
|
||||
}
|
||||
|
||||
// Return the new list
|
||||
return infiles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of just files from inputs
|
||||
/// </summary>
|
||||
/// <param name="inputs">List of strings representing directories and files</param>
|
||||
/// <param name="appendparent">True if the parent name should be be included in the ParentablePath, false otherwise (default)</param>
|
||||
/// <returns>List of strings representing just files from the inputs</returns>
|
||||
public static List<ParentablePath> GetFilesOnly(List<string> inputs, bool appendparent = false)
|
||||
{
|
||||
List<ParentablePath> outputs = new List<ParentablePath>();
|
||||
foreach (string input in inputs)
|
||||
{
|
||||
// Get the parent path in case of appending
|
||||
string parentPath = Path.GetFullPath(input);
|
||||
|
||||
if (Directory.Exists(input))
|
||||
{
|
||||
List<string> files = GetFilesOrdered(input);
|
||||
foreach (string file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
outputs.Add(new ParentablePath(Path.GetFullPath(file), appendparent ? parentPath : string.Empty));
|
||||
}
|
||||
catch (PathTooLongException)
|
||||
{
|
||||
Globals.Logger.Warning($"The path for '{file}' was too long");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Globals.Logger.Error(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (File.Exists(input))
|
||||
{
|
||||
try
|
||||
{
|
||||
outputs.Add(new ParentablePath(Path.GetFullPath(input), appendparent ? parentPath : string.Empty));
|
||||
}
|
||||
catch (PathTooLongException)
|
||||
{
|
||||
Globals.Logger.Warning($"The path for '{input}' was too long");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Globals.Logger.Error(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of files from a directory recursively in proper order
|
||||
/// </summary>
|
||||
/// <param name="dir">Directory to parse</param>
|
||||
/// <param name="infiles">List representing existing files</param>
|
||||
/// <returns>List with all new files</returns>
|
||||
public static List<string> GetFilesOrdered(string dir)
|
||||
{
|
||||
return GetFilesOrderedHelper(dir, new List<string>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of files from a directory recursively in proper order
|
||||
/// </summary>
|
||||
/// <param name="dir">Directory to parse</param>
|
||||
/// <param name="infiles">List representing existing files</param>
|
||||
/// <returns>List with all new files</returns>
|
||||
private static List<string> GetFilesOrderedHelper(string dir, List<string> infiles)
|
||||
{
|
||||
// Take care of the files in the top directory
|
||||
List<string> toadd = Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly).ToList();
|
||||
toadd.Sort(new NaturalComparer());
|
||||
infiles.AddRange(toadd);
|
||||
|
||||
// Then recurse through and add from the directories
|
||||
List<string> subDirs = Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly).ToList();
|
||||
subDirs.Sort(new NaturalComparer());
|
||||
foreach (string subdir in subDirs)
|
||||
{
|
||||
infiles = GetFilesOrderedHelper(subdir, infiles);
|
||||
}
|
||||
|
||||
// Return the new list
|
||||
return infiles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all empty folders within a root folder
|
||||
/// </summary>
|
||||
/// <param name="root">Root directory to parse</param>
|
||||
/// <returns>IEumerable containing all directories that are empty, an empty enumerable if the root is empty, null otherwise</returns>
|
||||
public static List<string> ListEmpty(string root)
|
||||
{
|
||||
// Check if the root exists first
|
||||
if (!Directory.Exists(root))
|
||||
return null;
|
||||
|
||||
// If it does and it is empty, return a blank enumerable
|
||||
if (Directory.EnumerateFileSystemEntries(root, "*", SearchOption.AllDirectories).Count() == 0)
|
||||
return new List<string>();
|
||||
|
||||
// Otherwise, get the complete list
|
||||
return Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories)
|
||||
.Where(dir => Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.AllDirectories).Count() == 0)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to safely delete a directory, optionally throwing the error
|
||||
/// </summary>
|
||||
/// <param name="file">Name of the directory to delete</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the file didn't exist or could be deleted, false otherwise</returns>
|
||||
public static bool TryCreateDirectory(string file, bool throwOnError = false)
|
||||
{
|
||||
// Now wrap creating the directory
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(file);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (throwOnError)
|
||||
throw ex;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to safely delete a directory, optionally throwing the error
|
||||
/// </summary>
|
||||
/// <param name="file">Name of the directory to delete</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the file didn't exist or could be deleted, false otherwise</returns>
|
||||
public static bool TryDelete(string file, bool throwOnError = false)
|
||||
{
|
||||
// Check if the directory exists first
|
||||
if (!Directory.Exists(file))
|
||||
return true;
|
||||
|
||||
// Now wrap deleting the directory
|
||||
try
|
||||
{
|
||||
Directory.Delete(file, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (throwOnError)
|
||||
throw ex;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,566 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
|
||||
using SabreTools.Library.Data;
|
||||
using SabreTools.Library.DatFiles;
|
||||
using SabreTools.Library.FileTypes;
|
||||
using SabreTools.Library.IO;
|
||||
using SabreTools.Library.Skippers;
|
||||
|
||||
namespace SabreTools.Library.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions to File functionality
|
||||
/// </summary>
|
||||
public static class FileExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an aribtrary number of bytes to the inputted file
|
||||
/// </summary>
|
||||
/// <param name="input">File to be appended to</param>
|
||||
/// <param name="output">Outputted file</param>
|
||||
/// <param name="bytesToAddToHead">Bytes to be added to head of file</param>
|
||||
/// <param name="bytesToAddToTail">Bytes to be added to tail of file</param>
|
||||
public 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 = TryOpenRead(input))
|
||||
using (FileStream fsw = TryOpenWrite(output))
|
||||
{
|
||||
#else
|
||||
using FileStream fsr = TryOpenRead(input);
|
||||
using FileStream fsw = TryOpenWrite(output);
|
||||
#endif
|
||||
StreamExtensions.AppendBytes(fsr, fsw, bytesToAddToHead, bytesToAddToTail);
|
||||
#if NET_FRAMEWORK
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get what type of DAT the input file is
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <returns>The DatFormat corresponding to the DAT</returns>
|
||||
public static DatFormat GetDatFormat(this string filename)
|
||||
{
|
||||
// Limit the output formats based on extension
|
||||
if (!PathExtensions.HasValidDatExtension(filename))
|
||||
return 0;
|
||||
|
||||
// Get the extension from the filename
|
||||
string ext = PathExtensions.GetNormalizedExtension(filename);
|
||||
|
||||
// Read the input file, if possible
|
||||
Globals.Logger.Verbose($"Attempting to read file to get format: {filename}");
|
||||
|
||||
// Check if file exists
|
||||
if (!File.Exists(filename))
|
||||
{
|
||||
Globals.Logger.Warning($"File '{filename}' could not read from!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Some formats should only require the extension to know
|
||||
switch (ext)
|
||||
{
|
||||
case "csv":
|
||||
return DatFormat.CSV;
|
||||
case "json":
|
||||
return DatFormat.Json;
|
||||
case "md5":
|
||||
return DatFormat.RedumpMD5;
|
||||
#if NET_FRAMEWORK
|
||||
case "ripemd160":
|
||||
return DatFormat.RedumpRIPEMD160;
|
||||
#endif
|
||||
case "sfv":
|
||||
return DatFormat.RedumpSFV;
|
||||
case "sha1":
|
||||
return DatFormat.RedumpSHA1;
|
||||
case "sha256":
|
||||
return DatFormat.RedumpSHA256;
|
||||
case "sha384":
|
||||
return DatFormat.RedumpSHA384;
|
||||
case "sha512":
|
||||
return DatFormat.RedumpSHA512;
|
||||
case "ssv":
|
||||
return DatFormat.SSV;
|
||||
case "tsv":
|
||||
return DatFormat.TSV;
|
||||
}
|
||||
|
||||
// For everything else, we need to read it
|
||||
try
|
||||
{
|
||||
// Get the first two non-whitespace, non-comment lines to check, if possible
|
||||
string first = string.Empty, second = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
using (StreamReader sr = File.OpenText(filename))
|
||||
{
|
||||
first = sr.ReadLine().ToLowerInvariant();
|
||||
while ((string.IsNullOrWhiteSpace(first) || first.StartsWith("<!--"))
|
||||
&& !sr.EndOfStream)
|
||||
{
|
||||
first = sr.ReadLine().ToLowerInvariant();
|
||||
}
|
||||
|
||||
if (!sr.EndOfStream)
|
||||
{
|
||||
second = sr.ReadLine().ToLowerInvariant();
|
||||
while (string.IsNullOrWhiteSpace(second) || second.StartsWith("<!--")
|
||||
&& !sr.EndOfStream)
|
||||
{
|
||||
second = sr.ReadLine().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// If we have an XML-based DAT
|
||||
if (first.Contains("<?xml") && first.Contains("?>"))
|
||||
{
|
||||
if (second.StartsWith("<!doctype datafile"))
|
||||
return DatFormat.Logiqx;
|
||||
|
||||
else if (second.StartsWith("<!doctype mame")
|
||||
|| second.StartsWith("<!doctype m1")
|
||||
|| second.StartsWith("<mame")
|
||||
|| second.StartsWith("<m1"))
|
||||
return DatFormat.Listxml;
|
||||
|
||||
else if (second.StartsWith("<!doctype softwaredb"))
|
||||
return DatFormat.OpenMSX;
|
||||
|
||||
else if (second.StartsWith("<!doctype softwarelist"))
|
||||
return DatFormat.SoftwareList;
|
||||
|
||||
else if (second.StartsWith("<!doctype sabredat"))
|
||||
return DatFormat.SabreDat;
|
||||
|
||||
else if ((second.StartsWith("<dat") && !second.StartsWith("<datafile"))
|
||||
|| second.StartsWith("<?xml-stylesheet"))
|
||||
return DatFormat.OfflineList;
|
||||
|
||||
// Older and non-compliant DATs
|
||||
else
|
||||
return DatFormat.Logiqx;
|
||||
}
|
||||
|
||||
// If we have an SMDB (SHA-256, Filename, SHA-1, MD5, CRC32)
|
||||
else if (Regex.IsMatch(first, @"[0-9a-f]{64}\t.*?\t[0-9a-f]{40}\t[0-9a-f]{32}\t[0-9a-f]{8}"))
|
||||
return DatFormat.EverdriveSMDB;
|
||||
|
||||
// If we have an INI-based DAT
|
||||
else if (first.Contains("[") && first.Contains("]"))
|
||||
return DatFormat.RomCenter;
|
||||
|
||||
// If we have a listroms DAT
|
||||
else if (first.StartsWith("roms required for driver"))
|
||||
return DatFormat.Listrom;
|
||||
|
||||
// If we have a CMP-based DAT
|
||||
else if (first.Contains("clrmamepro"))
|
||||
return DatFormat.ClrMamePro;
|
||||
|
||||
else if (first.Contains("romvault"))
|
||||
return DatFormat.ClrMamePro;
|
||||
|
||||
else if (first.Contains("doscenter"))
|
||||
return DatFormat.DOSCenter;
|
||||
|
||||
else if (first.Contains("#Name;Title;Emulator;CloneOf;Year;Manufacturer;Category;Players;Rotation;Control;Status;DisplayCount;DisplayType;AltRomname;AltTitle;Extra"))
|
||||
return DatFormat.AttractMode;
|
||||
|
||||
else
|
||||
return DatFormat.ClrMamePro;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines a text file's encoding by analyzing its byte order mark (BOM).
|
||||
/// Defaults to ASCII when detection of the text file's endianness fails.
|
||||
/// </summary>
|
||||
/// <param name="filename">The text file to analyze.</param>
|
||||
/// <returns>The detected encoding.</returns>
|
||||
/// <link>http://stackoverflow.com/questions/3825390/effective-way-to-find-any-files-encoding</link>
|
||||
public static Encoding GetEncoding(string filename)
|
||||
{
|
||||
// Read the BOM
|
||||
var bom = new byte[4];
|
||||
FileStream file = FileExtensions.TryOpenRead(filename);
|
||||
file.Read(bom, 0, 4);
|
||||
file.Dispose();
|
||||
|
||||
// Analyze the BOM
|
||||
if (bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76) return Encoding.UTF7;
|
||||
if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf) return Encoding.UTF8;
|
||||
if (bom[0] == 0xff && bom[1] == 0xfe) return Encoding.Unicode; //UTF-16LE
|
||||
if (bom[0] == 0xfe && bom[1] == 0xff) return Encoding.BigEndianUnicode; //UTF-16BE
|
||||
if (bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff) return Encoding.UTF32;
|
||||
return Encoding.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file type of an input file
|
||||
/// </summary>
|
||||
/// <param name="input">Input file to check</param>
|
||||
/// <returns>FileType of inputted file (null on error)</returns>
|
||||
public static FileType? GetFileType(this string input)
|
||||
{
|
||||
FileType? outFileType = null;
|
||||
|
||||
// If the file is null, then we have no archive type
|
||||
if (input == null)
|
||||
return outFileType;
|
||||
|
||||
// First line of defense is going to be the extension, for better or worse
|
||||
if (!PathExtensions.HasValidArchiveExtension(input))
|
||||
return outFileType;
|
||||
|
||||
// Read the first bytes of the file and get the magic number
|
||||
try
|
||||
{
|
||||
byte[] magic = new byte[8];
|
||||
BinaryReader br = new BinaryReader(TryOpenRead(input));
|
||||
magic = br.ReadBytes(8);
|
||||
br.Dispose();
|
||||
|
||||
// Now try to match it to a known signature
|
||||
if (magic.StartsWith(Constants.SevenZipSignature))
|
||||
{
|
||||
outFileType = FileType.SevenZipArchive;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.CHDSignature))
|
||||
{
|
||||
outFileType = FileType.CHD;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.GzSignature))
|
||||
{
|
||||
outFileType = FileType.GZipArchive;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.LRZipSignature))
|
||||
{
|
||||
outFileType = FileType.LRZipArchive;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.LZ4Signature)
|
||||
|| magic.StartsWith(Constants.LZ4SkippableMinSignature)
|
||||
|| magic.StartsWith(Constants.LZ4SkippableMaxSignature))
|
||||
{
|
||||
outFileType = FileType.LZ4Archive;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.RarSignature)
|
||||
|| magic.StartsWith(Constants.RarFiveSignature))
|
||||
{
|
||||
outFileType = FileType.RarArchive;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.TarSignature)
|
||||
|| magic.StartsWith(Constants.TarZeroSignature))
|
||||
{
|
||||
outFileType = FileType.TapeArchive;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.XZSignature))
|
||||
{
|
||||
outFileType = FileType.XZArchive;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.ZipSignature)
|
||||
|| magic.StartsWith(Constants.ZipSignatureEmpty)
|
||||
|| magic.StartsWith(Constants.ZipSignatureSpanned))
|
||||
{
|
||||
outFileType = FileType.ZipArchive;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.ZPAQSignature))
|
||||
{
|
||||
outFileType = FileType.ZPAQArchive;
|
||||
}
|
||||
else if (magic.StartsWith(Constants.ZstdSignature))
|
||||
{
|
||||
outFileType = FileType.ZstdArchive;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Don't log file open errors
|
||||
}
|
||||
|
||||
return outFileType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if the first byte array starts with the second array
|
||||
/// </summary>
|
||||
/// <param name="arr1">First byte array to compare</param>
|
||||
/// <param name="arr2">Second byte array to compare</param>
|
||||
/// <param name="exact">True if the input arrays should match exactly, false otherwise (default)</param>
|
||||
/// <returns>True if the first byte array starts with the second, false otherwise</returns>
|
||||
private static bool StartsWith(this byte[] arr1, byte[] arr2, bool exact = false)
|
||||
{
|
||||
// If we have any invalid inputs, we return false
|
||||
if (arr1 == null || arr2 == null
|
||||
|| arr1.Length == 0 || arr2.Length == 0
|
||||
|| arr2.Length > arr1.Length
|
||||
|| (exact && arr1.Length != arr2.Length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, loop through and see
|
||||
for (int i = 0; i < arr2.Length; i++)
|
||||
{
|
||||
if (arr1[i] != arr2[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve file information for a single file
|
||||
/// </summary>
|
||||
/// <param name="input">Filename to get information from</param>
|
||||
/// <param name="omitFromScan">Hash flag saying what hashes should not be calculated (defaults to none)</param>
|
||||
/// <param name="date">True if the file Date should be included, false otherwise (default)</param>
|
||||
/// <param name="header">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
|
||||
/// <param name="chdsAsFiles">True if CHDs should be treated like regular files, false otherwise</param>
|
||||
/// <returns>Populated BaseFile object if success, empty one on error</returns>
|
||||
public static BaseFile GetInfo(string input, Hash omitFromScan = 0x0, bool date = false, string header = null, bool chdsAsFiles = true)
|
||||
{
|
||||
// Add safeguard if file doesn't exist
|
||||
if (!File.Exists(input))
|
||||
return null;
|
||||
|
||||
// Get the information from the file stream
|
||||
BaseFile baseFile;
|
||||
if (header != null)
|
||||
{
|
||||
SkipperRule rule = Transform.GetMatchingRule(input, Path.GetFileNameWithoutExtension(header));
|
||||
|
||||
// If there's a match, get the new information from the stream
|
||||
if (rule.Tests != null && rule.Tests.Count != 0)
|
||||
{
|
||||
// Create the input and output streams
|
||||
MemoryStream outputStream = new MemoryStream();
|
||||
FileStream inputStream = TryOpenRead(input);
|
||||
|
||||
// Transform the stream and get the information from it
|
||||
rule.TransformStream(inputStream, outputStream, keepReadOpen: false, keepWriteOpen: true);
|
||||
baseFile = outputStream.GetInfo(omitFromScan: omitFromScan, keepReadOpen: false, chdsAsFiles: chdsAsFiles);
|
||||
|
||||
// Dispose of the streams
|
||||
outputStream.Dispose();
|
||||
inputStream.Dispose();
|
||||
}
|
||||
// Otherwise, just get the info
|
||||
else
|
||||
{
|
||||
baseFile = TryOpenRead(input).GetInfo(omitFromScan: omitFromScan, keepReadOpen: false, chdsAsFiles: chdsAsFiles);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
baseFile = TryOpenRead(input).GetInfo(omitFromScan: omitFromScan, keepReadOpen: false, chdsAsFiles: chdsAsFiles);
|
||||
}
|
||||
|
||||
// Add unique data from the file
|
||||
baseFile.Filename = Path.GetFileName(input);
|
||||
baseFile.Date = (date ? new FileInfo(input).LastWriteTime.ToString("yyyy/MM/dd HH:mm:ss") : string.Empty);
|
||||
|
||||
return baseFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the IniReader associated with a file, if possible
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="validateRows">True if rows should be in a proper format, false if invalid is okay</param>
|
||||
/// <returns>The IniReader representing the (possibly converted) file, null otherwise</returns>
|
||||
public static IniReader GetIniReader(this string filename, bool validateRows)
|
||||
{
|
||||
Globals.Logger.Verbose($"Attempting to read file: {filename}");
|
||||
|
||||
// Check if file exists
|
||||
if (!File.Exists(filename))
|
||||
{
|
||||
Globals.Logger.Warning($"File '{filename}' could not read from!");
|
||||
return null;
|
||||
}
|
||||
|
||||
IniReader ir = new IniReader(filename)
|
||||
{
|
||||
ValidateRows = validateRows
|
||||
};
|
||||
return ir;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the XmlTextReader associated with a file, if possible
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <returns>The XmlTextReader representing the (possibly converted) file, null otherwise</returns>
|
||||
public static XmlReader GetXmlTextReader(this string filename)
|
||||
{
|
||||
Globals.Logger.Verbose($"Attempting to read file: {filename}");
|
||||
|
||||
// Check if file exists
|
||||
if (!File.Exists(filename))
|
||||
{
|
||||
Globals.Logger.Warning($"File '{filename}' could not read from!");
|
||||
return null;
|
||||
}
|
||||
|
||||
XmlReader xtr = XmlReader.Create(filename, new XmlReaderSettings
|
||||
{
|
||||
CheckCharacters = false,
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
IgnoreComments = true,
|
||||
IgnoreWhitespace = true,
|
||||
ValidationFlags = XmlSchemaValidationFlags.None,
|
||||
ValidationType = ValidationType.None,
|
||||
});
|
||||
|
||||
return xtr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to create a file for write, optionally throwing the error
|
||||
/// </summary>
|
||||
/// <param name="file">Name of the file to create</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>An opened stream representing the file on success, null otherwise</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to safely delete a file, optionally throwing the error
|
||||
/// </summary>
|
||||
/// <param name="file">Name of the file to delete</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the file didn't exist or could be deleted, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to open a file for read, optionally throwing the error
|
||||
/// </summary>
|
||||
/// <param name="file">Name of the file to open</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>An opened stream representing the file on success, null otherwise</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to open a file for read/write, optionally throwing the error
|
||||
/// </summary>
|
||||
/// <param name="file">Name of the file to open</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>An opened stream representing the file on success, null otherwise</returns>
|
||||
public static FileStream TryOpenReadWrite(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.ReadWrite, FileShare.ReadWrite);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (throwOnError)
|
||||
throw ex;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to open an existing file for write, optionally throwing the error
|
||||
/// </summary>
|
||||
/// <param name="file">Name of the file to open</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>An opened stream representing the file on success, null otherwise</returns>
|
||||
public static FileStream TryOpenWrite(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.Write, FileShare.ReadWrite);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (throwOnError)
|
||||
throw ex;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Library.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// A path that optionally contains a parent root
|
||||
/// </summary>
|
||||
public class ParentablePath
|
||||
{
|
||||
/// <summary>
|
||||
/// Current full path represented
|
||||
/// </summary>
|
||||
public string CurrentPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Possible parent path represented (may be null or empty)
|
||||
/// </summary>
|
||||
public string ParentPath { get; private set; }
|
||||
|
||||
public ParentablePath(string currentPath, string parentPath = null)
|
||||
{
|
||||
CurrentPath = currentPath;
|
||||
ParentPath = parentPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the proper filename (with subpath) from the file and parent combination
|
||||
/// </summary>
|
||||
/// <param name="sanitize">True if path separators should be converted to '-', false otherwise</param>
|
||||
/// <returns>Subpath for the file</returns>
|
||||
public string GetNormalizedFileName(bool sanitize)
|
||||
{
|
||||
// Check that we have a combined path first
|
||||
if (string.IsNullOrWhiteSpace(ParentPath))
|
||||
{
|
||||
string filename = Path.GetFileName(CurrentPath);
|
||||
if (sanitize)
|
||||
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
// If the parts are the same, return the filename from the first part
|
||||
if (string.Equals(CurrentPath, ParentPath, StringComparison.Ordinal))
|
||||
{
|
||||
string filename = Path.GetFileName(CurrentPath);
|
||||
if (sanitize)
|
||||
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
// Otherwise, remove the path.ParentPath from the path.CurrentPath and return the remainder
|
||||
else
|
||||
{
|
||||
string filename = CurrentPath.Remove(0, ParentPath.Length + 1);
|
||||
if (sanitize)
|
||||
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
|
||||
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the proper output path for a given input file and output directory
|
||||
/// </summary>
|
||||
/// <param name="outDir">Output directory to use</param>
|
||||
/// <param name="inplace">True if the output file should go to the same input folder, false otherwise</param>
|
||||
/// <returns>Complete output path</returns>
|
||||
public string GetOutputPath(string outDir, bool inplace)
|
||||
{
|
||||
// First, we need to ensure the output directory
|
||||
outDir = DirectoryExtensions.Ensure(outDir);
|
||||
|
||||
// Check if we have a split path or not
|
||||
bool splitpath = !string.IsNullOrWhiteSpace(ParentPath);
|
||||
|
||||
// If we have a split path, we need to treat the input separately
|
||||
if (splitpath)
|
||||
{
|
||||
// If we have an inplace output, use the directory name from the input path
|
||||
if (inplace)
|
||||
{
|
||||
outDir = Path.GetDirectoryName(CurrentPath);
|
||||
}
|
||||
|
||||
// TODO: Should this be the default? Always create a subfolder if a folder is found?
|
||||
// If we are processing a path that is coming from a directory and we are outputting to the current directory, we want to get the subfolder to write to
|
||||
else if (CurrentPath.Length != ParentPath.Length && outDir == Environment.CurrentDirectory)
|
||||
{
|
||||
outDir = Path.GetDirectoryName(Path.Combine(outDir, CurrentPath.Remove(0, Path.GetDirectoryName(ParentPath).Length + 1)));
|
||||
}
|
||||
|
||||
// If we are processing a path that is coming from a directory, we want to get the subfolder to write to
|
||||
else if (CurrentPath.Length != ParentPath.Length)
|
||||
{
|
||||
outDir = Path.GetDirectoryName(Path.Combine(outDir, CurrentPath.Remove(0, ParentPath.Length + 1)));
|
||||
}
|
||||
|
||||
// If we are processing a single file from the root of a directory, we just use the output directory
|
||||
else
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
// Otherwise, assume the input path is just a filename
|
||||
else
|
||||
{
|
||||
// If we have an inplace output, use the directory name from the input path
|
||||
if (inplace)
|
||||
{
|
||||
outDir = Path.GetDirectoryName(CurrentPath);
|
||||
}
|
||||
|
||||
// Otherwise, just use the supplied output directory
|
||||
else
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, return the output directory
|
||||
return outDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
using SabreTools.Library.Data;
|
||||
|
||||
namespace SabreTools.Library.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions to Path functionality
|
||||
/// </summary>
|
||||
public static class PathExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the extension from the path, if possible
|
||||
/// </summary>
|
||||
/// <param name="path">Path to get extension from</param>
|
||||
/// <returns>Extension, if possible</returns>
|
||||
public static string GetNormalizedExtension(string path)
|
||||
{
|
||||
// Check null or empty first
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return null;
|
||||
|
||||
// Get the extension from the path, if possible
|
||||
string ext = Path.GetExtension(path)?.ToLowerInvariant();
|
||||
|
||||
// Check if the extension is null or empty
|
||||
if (string.IsNullOrWhiteSpace(ext))
|
||||
return null;
|
||||
|
||||
// Make sure that extensions are valid
|
||||
ext = ext.TrimStart('.');
|
||||
|
||||
return ext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a proper romba sub path
|
||||
/// </summary>
|
||||
/// <param name="hash">SHA-1 hash to get the path for</param>
|
||||
/// <returns>Subfolder path for the given hash</returns>
|
||||
public static string GetRombaPath(string hash)
|
||||
{
|
||||
// If the hash isn't the right size, then we return null
|
||||
if (hash.Length != Constants.SHA1Length) // TODO: When updating to SHA-256, this needs to update to Constants.SHA256Length
|
||||
return null;
|
||||
|
||||
return Path.Combine(hash.Substring(0, 2), hash.Substring(2, 2), hash.Substring(4, 2), hash.Substring(6, 2), hash + ".gz");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if the given path has a valid DAT extension
|
||||
/// </summary>
|
||||
/// <param name="path">Path to check</param>
|
||||
/// <returns>True if the extension is valid, false otherwise</returns>
|
||||
public static bool HasValidArchiveExtension(string path)
|
||||
{
|
||||
// Get the extension from the path, if possible
|
||||
string ext = PathExtensions.GetNormalizedExtension(path);
|
||||
|
||||
// Check against the list of known archive extensions
|
||||
switch (ext)
|
||||
{
|
||||
case "7z":
|
||||
case "gz":
|
||||
case "lzma":
|
||||
case "rar":
|
||||
case "rev":
|
||||
case "r00":
|
||||
case "r01":
|
||||
case "tar":
|
||||
case "tgz":
|
||||
case "tlz":
|
||||
case "zip":
|
||||
case "zipx":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if the given path has a valid DAT extension
|
||||
/// </summary>
|
||||
/// <param name="path">Path to check</param>
|
||||
/// <returns>True if the extension is valid, false otherwise</returns>
|
||||
public static bool HasValidDatExtension(string path)
|
||||
{
|
||||
// Get the extension from the path, if possible
|
||||
string ext = GetNormalizedExtension(path);
|
||||
|
||||
// Check against the list of known DAT extensions
|
||||
switch (ext)
|
||||
{
|
||||
case "csv":
|
||||
case "dat":
|
||||
case "json":
|
||||
case "md5":
|
||||
case "ripemd160":
|
||||
case "sfv":
|
||||
case "sha1":
|
||||
case "sha256":
|
||||
case "sha384":
|
||||
case "sha512":
|
||||
case "ssv":
|
||||
case "tsv":
|
||||
case "txt":
|
||||
case "xml":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
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 Compress.ThreadReaders;
|
||||
|
||||
namespace SabreTools.Library.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions to Stream functionality
|
||||
/// </summary>
|
||||
public static class StreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an aribtrary number of bytes to the inputted stream
|
||||
/// </summary>
|
||||
/// <param name="input">Stream to be appended to</param>
|
||||
/// <param name="output">Outputted stream</param>
|
||||
/// <param name="bytesToAddToHead">Bytes to be added to head of stream</param>
|
||||
/// <param name="bytesToAddToTail">Bytes to be added to tail of stream</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve file information for a single file
|
||||
/// </summary>
|
||||
/// <param name="input">Filename to get information from</param>
|
||||
/// <param name="size">Size of the input stream</param>
|
||||
/// <param name="omitFromScan">Hash flag saying what hashes should not be calculated (defaults to none)</param>
|
||||
/// <param name="keepReadOpen">True if the underlying read stream should be kept open, false otherwise</param>
|
||||
/// <param name="chdsAsFiles">True if CHDs should be treated like regular files, false otherwise</param>
|
||||
/// <returns>Populated BaseFile object if success, empty one on error</returns>
|
||||
public static BaseFile GetInfo(this Stream input, long size = -1, Hash omitFromScan = 0x0, bool keepReadOpen = false, bool chdsAsFiles = true)
|
||||
{
|
||||
return GetInfoAsync(input, size, omitFromScan, keepReadOpen, chdsAsFiles).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve file information for a single file
|
||||
/// </summary>
|
||||
/// <param name="input">Filename to get information from</param>
|
||||
/// <param name="size">Size of the input stream</param>
|
||||
/// <param name="omitFromScan">Hash flag saying what hashes should not be calculated (defaults to none)</param>
|
||||
/// <param name="keepReadOpen">True if the underlying read stream should be kept open, false otherwise</param>
|
||||
/// <param name="chdsAsFiles">True if CHDs should be treated like regular files, false otherwise</param>
|
||||
/// <returns>Populated BaseFile object if success, empty one on error</returns>
|
||||
public static async Task<BaseFile> GetInfoAsync(Stream input, long size = -1, Hash omitFromScan = 0x0, bool keepReadOpen = false, bool chdsAsFiles = true)
|
||||
{
|
||||
// If we want to automatically set the size
|
||||
if (size == -1)
|
||||
size = input.Length;
|
||||
|
||||
// We first check to see if it's a CHD if we have to
|
||||
if (!chdsAsFiles)
|
||||
{
|
||||
var chd = CHDFile.Create(input);
|
||||
input.SeekIfPossible();
|
||||
|
||||
// If we found a valid CHD
|
||||
if (chd != null)
|
||||
{
|
||||
if (!keepReadOpen)
|
||||
input.Dispose();
|
||||
|
||||
return chd;
|
||||
}
|
||||
}
|
||||
|
||||
BaseFile rom = new BaseFile()
|
||||
{
|
||||
Size = size,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Get a list of hashers to run over the buffer
|
||||
List<Hasher> hashers = new List<Hasher>()
|
||||
{
|
||||
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),
|
||||
};
|
||||
|
||||
// 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
|
||||
int next = size > buffersize ? buffersize : (int)size;
|
||||
input.Read(buffer0, 0, next);
|
||||
int current = next;
|
||||
size -= next;
|
||||
bool bufferSelect = true;
|
||||
|
||||
while (current > 0)
|
||||
{
|
||||
// Trigger the buffer load on the second buffer
|
||||
next = size > buffersize ? buffersize : (int)size;
|
||||
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;
|
||||
size -= next;
|
||||
bufferSelect = !bufferSelect;
|
||||
}
|
||||
|
||||
// Finalize all hashing helpers
|
||||
loadBuffer.Finish();
|
||||
await Task.WhenAll(hashers.Select(h => h.Finalize()));
|
||||
|
||||
// Get the results
|
||||
if (!omitFromScan.HasFlag(Hash.CRC))
|
||||
rom.CRC = hashers.First(h => h.HashType == Hash.CRC).GetHash();
|
||||
if (!omitFromScan.HasFlag(Hash.MD5))
|
||||
rom.MD5 = hashers.First(h => h.HashType == Hash.MD5).GetHash();
|
||||
#if NET_FRAMEWORK
|
||||
if (!omitFromScan.HasFlag(Hash.RIPEMD160))
|
||||
rom.RIPEMD160 = hashers.First(h => h.HashType == Hash.RIPEMD160).GetHash();
|
||||
#endif
|
||||
if (!omitFromScan.HasFlag(Hash.SHA1))
|
||||
rom.SHA1 = hashers.First(h => h.HashType == Hash.SHA1).GetHash();
|
||||
if (!omitFromScan.HasFlag(Hash.SHA256))
|
||||
rom.SHA256 = hashers.First(h => h.HashType == Hash.SHA256).GetHash();
|
||||
if (!omitFromScan.HasFlag(Hash.SHA384))
|
||||
rom.SHA384 = hashers.First(h => h.HashType == Hash.SHA384).GetHash();
|
||||
if (!omitFromScan.HasFlag(Hash.SHA512))
|
||||
rom.SHA512 = hashers.First(h => h.HashType == Hash.SHA512).GetHash();
|
||||
|
||||
// Dispose of the hashers
|
||||
loadBuffer.Dispose();
|
||||
hashers.ForEach(h => h.Dispose());
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return new BaseFile();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!keepReadOpen)
|
||||
input.Dispose();
|
||||
else
|
||||
input.SeekIfPossible();
|
||||
}
|
||||
|
||||
return rom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seek to a specific point in the stream, if possible
|
||||
/// </summary>
|
||||
/// <param name="input">Input stream to try seeking on</param>
|
||||
/// <param name="offset">Optional offset to seek to</param>
|
||||
private static long SeekIfPossible(this Stream input, long offset = 0)
|
||||
{
|
||||
if (input.CanSeek)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (offset < 0)
|
||||
return input.Seek(offset, SeekOrigin.End);
|
||||
else if (offset >= 0)
|
||||
return input.Seek(offset, SeekOrigin.Begin);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
Globals.Logger.Verbose("Stream does not support seeking to starting offset. Stream position not changed");
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
Globals.Logger.Warning("Stream does not support seeking to starting offset. Stream position not changed");
|
||||
}
|
||||
}
|
||||
|
||||
return input.Position;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user