You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

288 lines
13 KiB

  1. #nullable enable
  2. using System;
  3. using System.IO;
  4. using System.Text;
  5. using System.Linq;
  6. using System.Collections.Generic;
  7. using Mono.Cecil;
  8. using System.Runtime.CompilerServices;
  9. using System.Threading;
  10. using System.Diagnostics.CodeAnalysis;
  11. using Version = Hive.Versioning.Version;
  12. #if NET3
  13. using File = Net3_Proxy.File;
  14. #endif
  15. namespace IPA.Utilities
  16. {
  17. /// <summary>
  18. /// A class providing static utility functions that in any other language would just *exist*.
  19. /// </summary>
  20. public static class Utils
  21. {
  22. /// <summary>
  23. /// Converts a hex string to a byte array.
  24. /// </summary>
  25. /// <param name="hex">the hex stream</param>
  26. /// <returns>the corresponding byte array</returns>
  27. public static byte[] StringToByteArray(string hex)
  28. {
  29. if (hex is null)
  30. throw new ArgumentNullException(nameof(hex));
  31. int numberChars = hex.Length;
  32. byte[] bytes = new byte[numberChars / 2];
  33. for (int i = 0; i < numberChars; i += 2)
  34. bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
  35. return bytes;
  36. }
  37. /// <summary>
  38. /// Converts a byte array to a hex string.
  39. /// </summary>
  40. /// <param name="ba">the byte array</param>
  41. /// <returns>the hex form of the array</returns>
  42. public static string ByteArrayToString(byte[] ba)
  43. {
  44. if (ba is null)
  45. throw new ArgumentNullException(nameof(ba));
  46. var hex = new StringBuilder(ba.Length * 2);
  47. foreach (byte b in ba)
  48. _ = hex.AppendFormat("{0:x2}", b);
  49. return hex.ToString();
  50. }
  51. // Copyright (c) 2008-2013 Hafthor Stefansson
  52. // Distributed under the MIT/X11 software license
  53. // Ref: http://www.opensource.org/licenses/mit-license.php.
  54. // From: https://stackoverflow.com/a/8808245/3117125
  55. /// <summary>
  56. /// Uses unsafe code to compare 2 byte arrays quickly.
  57. /// </summary>
  58. /// <param name="a1">array 1</param>
  59. /// <param name="a2">array 2</param>
  60. /// <returns>whether or not they are byte-for-byte equal</returns>
  61. public static unsafe bool UnsafeCompare(byte[] a1, byte[] a2)
  62. {
  63. if (a1 == a2) return true;
  64. if (a1 == null || a2 == null || a1.Length != a2.Length)
  65. return false;
  66. fixed (byte* p1 = a1, p2 = a2)
  67. {
  68. byte* x1 = p1, x2 = p2;
  69. int l = a1.Length;
  70. for (int i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
  71. if (*(long*)x1 != *(long*)x2) return false;
  72. if ((l & 4) != 0) { if (*(int*)x1 != *(int*)x2) return false; x1 += 4; x2 += 4; }
  73. if ((l & 2) != 0) { if (*(short*)x1 != *(short*)x2) return false; x1 += 2; x2 += 2; }
  74. if ((l & 1) != 0) if (*x1 != *x2) return false;
  75. return true;
  76. }
  77. }
  78. /// <summary>
  79. /// Gets a path relative to the provided folder.
  80. /// </summary>
  81. /// <param name="file">the file to relativize</param>
  82. /// <param name="folder">the source folder</param>
  83. /// <returns>a path to get from <paramref name="folder"/> to <paramref name="file"/></returns>
  84. public static string GetRelativePath(string file, string folder)
  85. {
  86. if (file is null)
  87. throw new ArgumentNullException(nameof(file));
  88. if (folder is null)
  89. throw new ArgumentNullException(nameof(folder));
  90. var pathUri = new Uri(file);
  91. // Folders must end in a slash
  92. if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
  93. {
  94. folder += Path.DirectorySeparatorChar;
  95. }
  96. var folderUri = new Uri(folder);
  97. return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar));
  98. }
  99. /// <summary>
  100. /// Copies all files from <paramref name="source"/> to <paramref name="target"/>.
  101. /// </summary>
  102. /// <param name="source">the source directory</param>
  103. /// <param name="target">the destination directory</param>
  104. /// <param name="appendFileName">the filename of the file to append together</param>
  105. /// <param name="onCopyException">a delegate called when there is an error copying. Return true to keep going.</param>
  106. public static void CopyAll(DirectoryInfo source, DirectoryInfo target, string appendFileName = "",
  107. Func<Exception, FileInfo, bool>? onCopyException = null)
  108. {
  109. if (source is null)
  110. throw new ArgumentNullException(nameof(source));
  111. if (target is null)
  112. throw new ArgumentNullException(nameof(target));
  113. if (source.FullName.ToUpperInvariant() == target.FullName.ToUpperInvariant())
  114. {
  115. return;
  116. }
  117. // Check if the target directory exists, if not, create it.
  118. if (Directory.Exists(target.FullName) == false)
  119. {
  120. _ = Directory.CreateDirectory(target.FullName);
  121. }
  122. // Copy each file into it's new directory.
  123. foreach (var fi in source.GetFiles())
  124. {
  125. try
  126. {
  127. if (fi.Name == appendFileName)
  128. File.AppendAllLines(Path.Combine(target.ToString(), fi.Name), File.ReadAllLines(fi.FullName));
  129. else
  130. _ = fi.CopyTo(Path.Combine(target.ToString(), fi.Name), true);
  131. }
  132. catch (Exception e)
  133. {
  134. var keepOn = onCopyException?.Invoke(e, fi);
  135. if (!keepOn.Unwrap())
  136. throw;
  137. }
  138. }
  139. // Copy each subdirectory using recursion.
  140. foreach (var diSourceSubDir in source.GetDirectories())
  141. {
  142. var nextTargetSubDir = target.CreateSubdirectory(diSourceSubDir.Name);
  143. CopyAll(diSourceSubDir, nextTargetSubDir, appendFileName, onCopyException);
  144. }
  145. }
  146. /// <summary>
  147. /// Whether you can safely use <see cref="DateTime.Now"/> without Mono throwing a fit.
  148. /// </summary>
  149. /// <value><see langword="true"/> if you can use <see cref="DateTime.Now"/> safely, <see langword="false"/> otherwise</value>
  150. public static bool CanUseDateTimeNowSafely { get; private set; } = true;
  151. private static bool DateTimeSafetyUnknown = true;
  152. /// <summary>
  153. /// Returns <see cref="DateTime.Now"/> if supported, otherwise <see cref="DateTime.UtcNow"/>.
  154. /// </summary>
  155. /// <returns>the current <see cref="DateTime"/> if supported, otherwise some indeterminant increasing value.</returns>
  156. public static DateTime CurrentTime()
  157. {
  158. if (DateTimeSafetyUnknown)
  159. {
  160. var time = DateTime.MinValue;
  161. try
  162. {
  163. time = DateTime.Now;
  164. }
  165. catch (TimeZoneNotFoundException)
  166. { // Mono did a fucky wucky and we need to avoid this call
  167. CanUseDateTimeNowSafely = false;
  168. }
  169. DateTimeSafetyUnknown = false;
  170. return time;
  171. }
  172. else
  173. {
  174. if (CanUseDateTimeNowSafely) return DateTime.Now;
  175. else return DateTime.UtcNow; // return MinValue as a fallback
  176. }
  177. }
  178. /// <summary>
  179. /// Compares a pair of <see cref="SemVer.Version"/>s ignoring both the prerelease and build fields.
  180. /// </summary>
  181. /// <param name="l">the left value</param>
  182. /// <param name="r">the right value</param>
  183. /// <returns>&lt; 0 if l is less than r, 0 if they are equal in the numeric portion, or &gt; 0 if l is greater than r</returns>
  184. [Obsolete("Use Hive.Versioning.Version overload instead.")]
  185. public static int VersionCompareNoPrerelease(SemVer.Version l, SemVer.Version r)
  186. => VersionCompareNoPrerelease(l?.UnderlyingVersion!, r?.UnderlyingVersion!);
  187. /// <summary>
  188. /// Compares a pair of <see cref="Version"/>s ignoring both the prerelease and build fields.
  189. /// </summary>
  190. /// <param name="l">the left value</param>
  191. /// <param name="r">the right value</param>
  192. /// <returns>&lt; 0 if l is less than r, 0 if they are equal in the numeric portion, or &gt; 0 if l is greater than r</returns>
  193. public static int VersionCompareNoPrerelease(Version l, Version r)
  194. {
  195. if (l is null) throw new ArgumentNullException(nameof(l));
  196. if (r is null) throw new ArgumentNullException(nameof(r));
  197. var cmpVal = l.Major.CompareTo(r.Major);
  198. if (cmpVal != 0) return cmpVal;
  199. cmpVal = l.Minor.CompareTo(r.Minor);
  200. if (cmpVal != 0) return cmpVal;
  201. cmpVal = l.Patch.CompareTo(r.Patch);
  202. return cmpVal;
  203. }
  204. /// <summary>
  205. /// An object used to manage scope guards.
  206. /// </summary>
  207. /// <example>
  208. /// <code>
  209. /// using var _ = new Utils.ScopeGuardObject(() => RunOnScopeExit(value));
  210. /// </code>
  211. /// </example>
  212. /// <seealso cref="ScopeGuard(Action)"/>
  213. [SuppressMessage("Design", "CA1034:Nested types should not be visible",
  214. Justification = "This type needs to be public to avoid allocations")]
  215. [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types",
  216. Justification = "This type is never supposed to be compared")]
  217. public struct ScopeGuardObject : IDisposable
  218. {
  219. private readonly Action action;
  220. /// <summary>
  221. /// Creates a new scope guard that will invoke <paramref name="action"/> when disposed.
  222. /// </summary>
  223. /// <param name="action">the action to run on dispose</param>
  224. public ScopeGuardObject(Action action)
  225. => this.action = action;
  226. void IDisposable.Dispose()
  227. => action?.Invoke();
  228. }
  229. /// <summary>
  230. /// Creates a scope guard for a given <see cref="Action"/>.
  231. /// </summary>
  232. /// <param name="action">the <see cref="Action"/> to run on dispose</param>
  233. /// <returns>a <see cref="ScopeGuardObject"/> that will run <paramref name="action"/> on disposal</returns>
  234. /// <example>
  235. /// <code>
  236. /// using var _ = Utils.ScopeGuard(() => RunOnScopeExit(value));
  237. /// </code>
  238. /// </example>
  239. public static ScopeGuardObject ScopeGuard(Action action)
  240. => new(action);
  241. /// <summary>
  242. /// Deconstructs a <see cref="KeyValuePair{TKey, TValue}"/> as its key and value.
  243. /// </summary>
  244. /// <typeparam name="TKey">The type of the key.</typeparam>
  245. /// <typeparam name="TValue">The type of the value.</typeparam>
  246. /// <param name="kvp">The <see cref="KeyValuePair{TKey, TValue}"/> to deconstruct.</param>
  247. /// <param name="key">The key in <paramref name="kvp"/>.</param>
  248. /// <param name="value">The value in <paramref name="kvp"/>.</param>
  249. public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
  250. => (key, value) = (kvp.Key, kvp.Value);
  251. internal static bool HasInterface(this TypeDefinition? type, string interfaceFullName)
  252. {
  253. return (type?.Interfaces?.Any(i => i.InterfaceType.FullName == interfaceFullName) ?? false)
  254. || (type?.Interfaces?.Any(t => HasInterface(t?.InterfaceType?.Resolve(), interfaceFullName)) ?? false);
  255. }
  256. #if NET4
  257. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  258. internal static IEnumerable<string> StrJP(this IEnumerable<string> a) => a;
  259. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  260. internal static IEnumerable<string> StrJP<T>(this IEnumerable<T> a) => a.Select(o => $"{o}" /* safer than .ToString() */);
  261. #endif
  262. #if NET3
  263. internal static string[] StrJP(this IEnumerable<string> a) => a.ToArray();
  264. internal static string[] StrJP<T>(this IEnumerable<T> a) => a.Select(o => $"{o}" /* safer than .ToString() */).ToArray();
  265. #endif
  266. }
  267. }