Files
HetCreep 4a879f582c Hash the full UTF-8 byte length in HashStrings (#1355)
HashStrings passed str.Length (a char count) as the byte count to
MD5.TransformBlock, so for any non-ASCII string only a truncated prefix
of the UTF-8 bytes was hashed. Capture the byte array and use its Length.
2026-06-21 12:20:41 +02:00

443 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Security.Cryptography;
using System.Text;
using BepInEx.Logging;
using Mono.Cecil;
namespace BepInEx;
/// <summary>
/// Generic helper properties and methods.
/// </summary>
public static class Utility
{
private const string TRUSTED_PLATFORM_ASSEMBLIES = "TRUSTED_PLATFORM_ASSEMBLIES";
private static bool? sreEnabled;
/// <summary>
/// Whether current Common Language Runtime supports dynamic method generation using
/// <see cref="System.Reflection.Emit" /> namespace.
/// </summary>
public static bool CLRSupportsDynamicAssemblies => CheckSRE();
/// <summary>
/// An encoding for UTF-8 which does not emit a byte order mark (BOM).
/// </summary>
public static Encoding UTF8NoBom { get; } = new UTF8Encoding(false);
private static bool CheckSRE()
{
try
{
if (sreEnabled.HasValue)
return sreEnabled.Value;
// ReSharper disable once AssignNullToNotNullAttribute
_ = new CustomAttributeBuilder(null, new object[0]);
}
catch (PlatformNotSupportedException)
{
sreEnabled = false;
return sreEnabled.Value;
}
catch (ArgumentNullException)
{
// Suppress ArgumentNullException
}
sreEnabled = true;
return sreEnabled.Value;
}
/// <summary>
/// Try to perform an action.
/// </summary>
/// <param name="action">Action to perform.</param>
/// <param name="exception">Possible exception that gets returned.</param>
/// <returns>True, if action succeeded, false if an exception occured.</returns>
public static bool TryDo(Action action, out Exception exception)
{
exception = null;
try
{
action();
return true;
}
catch (Exception e)
{
exception = e;
return false;
}
}
/// <summary>
/// Combines multiple paths together, as the specific method is not available in .NET 3.5.
/// </summary>
/// <param name="parts">The multiple paths to combine together.</param>
/// <returns>A combined path.</returns>
public static string CombinePaths(params string[] parts) => parts.Aggregate(Path.Combine);
/// <summary>
/// Returns the parent directory of a path, optionally specifying the amount of levels.
/// </summary>
/// <param name="path">The path to get the parent directory of.</param>
/// <param name="levels">The amount of levels to traverse. Defaults to 1</param>
/// <returns>The parent directory.</returns>
public static string ParentDirectory(string path, int levels = 1)
{
for (var i = 0; i < levels; i++)
path = Path.GetDirectoryName(path);
return path;
}
/// <summary>
/// Tries to parse a bool, with a default value if unable to parse.
/// </summary>
/// <param name="input">The string to parse</param>
/// <param name="defaultValue">The value to return if parsing is unsuccessful.</param>
/// <returns>Boolean value of input if able to be parsed, otherwise default value.</returns>
public static bool SafeParseBool(string input, bool defaultValue = false) =>
bool.TryParse(input, out var result) ? result : defaultValue;
/// <summary>
/// Converts a file path into a UnityEngine.WWW format.
/// </summary>
/// <param name="path">The file path to convert.</param>
/// <returns>A converted file path.</returns>
public static string ConvertToWWWFormat(string path) => $"file://{path.Replace('\\', '/')}";
/// <summary>
/// Indicates whether a specified string is null, empty, or consists only of white-space characters.
/// </summary>
/// <param name="self">The string to test.</param>
/// <returns>True if the value parameter is null or empty, or if value consists exclusively of white-space characters.</returns>
public static bool IsNullOrWhiteSpace(this string self) => self == null || self.All(char.IsWhiteSpace);
/// <summary>
/// Sorts a given dependency graph using a direct toposort, reporting possible cyclic dependencies.
/// </summary>
/// <param name="nodes">Nodes to sort</param>
/// <param name="dependencySelector">Function that maps a node to a collection of its dependencies.</param>
/// <typeparam name="TNode">Type of the node in a dependency graph.</typeparam>
/// <returns>Collection of nodes sorted in the order of least dependencies to the most.</returns>
/// <exception cref="Exception">Thrown when a cyclic dependency occurs.</exception>
public static IEnumerable<TNode> TopologicalSort<TNode>(IEnumerable<TNode> nodes,
Func<TNode, IEnumerable<TNode>> dependencySelector)
{
var sorted_list = new List<TNode>();
var visited = new HashSet<TNode>();
var sorted = new HashSet<TNode>();
foreach (var input in nodes)
{
var currentStack = new Stack<TNode>();
if (!Visit(input, currentStack))
throw new Exception("Cyclic Dependency:\r\n" + currentStack.Select(x => $" - {x}") //append dashes
.Aggregate((a, b) =>
$"{a}\r\n{b}")); //add new lines inbetween
}
return sorted_list;
bool Visit(TNode node, Stack<TNode> stack)
{
if (visited.Contains(node))
{
if (!sorted.Contains(node)) return false;
}
else
{
visited.Add(node);
stack.Push(node);
if (dependencySelector(node).Any(dep => !Visit(dep, stack)))
return false;
sorted.Add(node);
sorted_list.Add(node);
stack.Pop();
}
return true;
}
}
/// <summary>
/// Try to resolve and load the given assembly DLL.
/// </summary>
/// <param name="assemblyName">Name of the assembly, of the type <see cref="AssemblyName" />.</param>
/// <param name="directory">Directory to search the assembly from.</param>
/// <param name="assembly">The loaded assembly.</param>
/// <returns>True, if the assembly was found and loaded. Otherwise, false.</returns>
public static bool TryResolveDllAssembly<T>(AssemblyName assemblyName,
string directory,
Func<string, T> loader,
out T assembly) where T : class
{
assembly = null;
var potentialDirectories = new List<string> { directory };
if (!Directory.Exists(directory))
return false;
potentialDirectories.AddRange(Directory.GetDirectories(directory, "*", SearchOption.AllDirectories));
foreach (var subDirectory in potentialDirectories)
{
var potentialPaths = new[]
{
$"{assemblyName.Name}.dll",
$"{assemblyName.Name}.exe"
};
foreach (var potentialPath in potentialPaths)
{
var path = Path.Combine(subDirectory, potentialPath);
if (!File.Exists(path))
continue;
try
{
assembly = loader(path);
}
catch (Exception)
{
continue;
}
return true;
}
}
return false;
}
/// <summary>
/// Checks whether a given cecil type definition is a subtype of a provided type.
/// </summary>
/// <param name="self">Cecil type definition</param>
/// <param name="td">Type to check against</param>
/// <returns>Whether the given cecil type is a subtype of the type.</returns>
public static bool IsSubtypeOf(this TypeDefinition self, Type td)
{
if (self.FullName == td.FullName)
return true;
return self.FullName != "System.Object" && (self.BaseType?.Resolve()?.IsSubtypeOf(td) ?? false);
}
/// <summary>
/// Try to resolve and load the given assembly DLL.
/// </summary>
/// <param name="assemblyName">Name of the assembly, of the type <see cref="AssemblyName" />.</param>
/// <param name="directory">Directory to search the assembly from.</param>
/// <param name="assembly">The loaded assembly.</param>
/// <returns>True, if the assembly was found and loaded. Otherwise, false.</returns>
public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly) =>
TryResolveDllAssembly(assemblyName, directory, Assembly.LoadFrom, out assembly);
/// <summary>
/// Try to resolve and load the given assembly DLL.
/// </summary>
/// <param name="assemblyName">Name of the assembly, of the type <see cref="AssemblyName" />.</param>
/// <param name="directory">Directory to search the assembly from.</param>
/// <param name="readerParameters">Reader parameters that contain possible custom assembly resolver.</param>
/// <param name="assembly">The loaded assembly.</param>
/// <returns>True, if the assembly was found and loaded. Otherwise, false.</returns>
public static bool TryResolveDllAssembly(AssemblyName assemblyName,
string directory,
ReaderParameters readerParameters,
out AssemblyDefinition assembly) =>
TryResolveDllAssembly(assemblyName, directory,
s => AssemblyDefinition.ReadAssembly(s, readerParameters), out assembly);
/// <summary>
/// Tries to create a file with the given name
/// </summary>
/// <param name="path">Path of the file to create</param>
/// <param name="mode">File open mode</param>
/// <param name="fileStream">Resulting filestream</param>
/// <param name="access">File access options</param>
/// <param name="share">File share options</param>
/// <returns></returns>
public static bool TryOpenFileStream(string path,
FileMode mode,
out FileStream fileStream,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.Read)
{
try
{
fileStream = new FileStream(path, mode, access, share);
return true;
}
catch (IOException)
{
fileStream = null;
return false;
}
}
public static IEnumerable<MethodDefinition> EnumerateAllMethods(this TypeDefinition type)
{
var currentType = type;
while (currentType != null)
{
foreach (var method in currentType.Methods)
yield return method;
currentType = currentType.BaseType?.Resolve();
}
}
/// <summary>
/// Compute a MD5 hash of the given stream.
/// </summary>
/// <param name="stream">Stream to hash</param>
/// <returns>MD5 hash as a hex string</returns>
public static string HashStream(Stream stream)
{
using var md5 = MD5.Create();
var buf = new byte[4096];
int read;
while ((read = stream.Read(buf, 0, buf.Length)) > 0)
md5.TransformBlock(buf, 0, read, buf, 0);
md5.TransformFinalBlock(new byte[0], 0, 0);
return ByteArrayToString(md5.Hash);
}
/// <summary>
/// Hash a list of strings using MD5
/// </summary>
/// <param name="strings">Strings to hash</param>
/// <returns>MD5 of the strings</returns>
public static string HashStrings(params string[] strings)
{
using var md5 = MD5.Create();
foreach (var str in strings)
{
var bytes = Encoding.UTF8.GetBytes(str);
md5.TransformBlock(bytes, 0, bytes.Length, null, 0);
}
md5.TransformFinalBlock(new byte[0], 0, 0);
return ByteArrayToString(md5.Hash);
}
/// <summary>
/// Convert the given array to a hex string.
/// </summary>
/// <param name="data">Bytes to convert.</param>
/// <returns>Bytes reinterpreted as a hex number.</returns>
public static string ByteArrayToString(byte[] data)
{
var builder = new StringBuilder(data.Length * 2);
foreach (var b in data)
builder.AppendFormat("{0:x2}", b);
return builder.ToString();
}
/// <summary>
/// Get a value of a command line argument
/// </summary>
/// <param name="arg">Argument name</param>
/// <returns>Next argument after the given argument name. If not found, returns null.</returns>
public static string GetCommandLineArgValue(string arg)
{
var args = Environment.GetCommandLineArgs();
for (var i = 1; i < args.Length; i++)
if (args[i] == arg && i + 1 < args.Length)
return args[i + 1];
return null;
}
/// <summary>
/// Try to parse given string as an assembly name
/// </summary>
/// <param name="fullName">Fully qualified assembly name</param>
/// <param name="assemblyName">Resulting <see cref="AssemblyName" /> instance</param>
/// <returns><c>true</c>, if parsing was successful, otherwise <c>false</c></returns>
/// <remarks>
/// On some versions of mono, using <see cref="Assembly.GetName()" /> fails because it runs on unmanaged side
/// which has problems with encoding.
/// Using <see cref="AssemblyName" /> solves this by doing parsing on managed side instead.
/// </remarks>
public static bool TryParseAssemblyName(string fullName, out AssemblyName assemblyName)
{
try
{
assemblyName = new AssemblyName(fullName);
return true;
}
catch (Exception)
{
assemblyName = null;
return false;
}
}
internal static void AddCecilPlatformAssemblies(this AppDomain appDomain, string assemblyDir)
{
if (!Directory.Exists(assemblyDir))
return;
// Cecil 0.11 requires one to manually set up list of trusted assemblies for assembly resolving
var curTrusted = appDomain.GetData(TRUSTED_PLATFORM_ASSEMBLIES) as string;
var addTrusted = string.Join(Path.PathSeparator.ToString(),
Directory.GetFiles(assemblyDir, "*.dll",
SearchOption.TopDirectoryOnly));
var newTrusted = curTrusted == null ? addTrusted : $"{curTrusted}{Path.PathSeparator}{addTrusted}";
appDomain.SetData(TRUSTED_PLATFORM_ASSEMBLIES, newTrusted);
}
/// <summary>
/// Gets unique files in all given directories. If the file with the same name exists in multiple directories,
/// only the first occurrence is returned.
/// </summary>
/// <param name="directories">Directories to search from.</param>
/// <param name="pattern">File pattern to search.</param>
/// <returns>Collection of all files in the directories.</returns>
public static IEnumerable<string> GetUniqueFilesInDirectories(IEnumerable<string> directories,
string pattern = "*")
{
var result = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
foreach (var directory in directories)
{
// A configured search directory (e.g. a missing unstripped_corlib) may not exist; warn and skip it
// instead of letting Directory.GetFiles throw DirectoryNotFoundException.
if (!Directory.Exists(directory))
{
Logger.Log(LogLevel.Warning, $"Skipping search directory that does not exist: {directory}");
continue;
}
foreach (var file in Directory.GetFiles(directory, pattern))
{
var fileName = Path.GetFileName(file);
if (!result.ContainsKey(fileName))
result[fileName] = file;
}
}
return result.Values;
}
}