1 // ----------------------------------------------------------------------------------------------
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
9 // Copyright 2015-2024 Ćukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
12 // Licensed under the Apache License, Version 2.0 (the "License");
13 // you may not use this file except in compliance with the License.
14 // You may obtain a copy of the License at
16 // http://www.apache.org/licenses/LICENSE-2.0
18 // Unless required by applicable law or agreed to in writing, software
19 // distributed under the License is distributed on an "AS IS" BASIS,
20 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 // See the License for the specific language governing permissions and
22 // limitations under the License.
25 using System
.Collections
;
26 using System
.Collections
.Frozen
;
27 using System
.Collections
.Generic
;
28 using System
.Diagnostics
.CodeAnalysis
;
29 using System
.Globalization
;
31 using System
.IO
.Compression
;
34 using System
.Resources
;
35 using System
.Security
.Cryptography
;
36 using System
.Threading
;
37 using System
.Threading
.Tasks
;
39 using AngleSharp
.XPath
;
40 using ArchiSteamFarm
.Localization
;
41 using ArchiSteamFarm
.NLog
;
42 using ArchiSteamFarm
.Storage
;
44 using JetBrains
.Annotations
;
45 using Microsoft
.IdentityModel
.JsonWebTokens
;
48 namespace ArchiSteamFarm
.Core
;
50 public static class Utilities
{
51 private const byte MaxSharingViolationTries
= 15;
52 private const uint SharingViolationHResult
= 0x80070020;
53 private const byte TimeoutForLongRunningTasksInSeconds
= 60;
54 private const uint UnauthorizedAccessHResult
= 0x80070005;
56 private static readonly FrozenSet
<char> DirectorySeparators
= new HashSet
<char>(2) { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }
.ToFrozenSet();
59 public static string GenerateChecksumFor(byte[] source
) {
60 ArgumentNullException
.ThrowIfNull(source
);
62 byte[] hash
= SHA512
.HashData(source
);
64 return Convert
.ToHexString(hash
);
68 public static string GetArgsAsText(string[] args
, byte argsToSkip
, string delimiter
) {
69 ArgumentNullException
.ThrowIfNull(args
);
71 if (args
.Length
<= argsToSkip
) {
72 throw new InvalidOperationException($"{nameof(args.Length)} && {nameof(argsToSkip)}");
75 ArgumentException
.ThrowIfNullOrEmpty(delimiter
);
77 return string.Join(delimiter
, args
.Skip(argsToSkip
));
81 public static string GetArgsAsText(string text
, byte argsToSkip
) {
82 ArgumentException
.ThrowIfNullOrEmpty(text
);
84 string[] args
= text
.Split(Array
.Empty
<char>(), argsToSkip
+ 1, StringSplitOptions
.RemoveEmptyEntries
);
90 public static string? GetCookieValue(this CookieContainer cookieContainer
, Uri uri
, string name
) {
91 ArgumentNullException
.ThrowIfNull(cookieContainer
);
92 ArgumentNullException
.ThrowIfNull(uri
);
93 ArgumentException
.ThrowIfNullOrEmpty(name
);
95 CookieCollection cookies
= cookieContainer
.GetCookies(uri
);
97 return cookies
.FirstOrDefault(cookie
=> cookie
.Name
== name
)?.Value
;
101 public static ulong GetUnixTime() => (ulong) DateTimeOffset
.UtcNow
.ToUnixTimeSeconds();
104 public static async void InBackground(Action action
, bool longRunning
= false) {
105 ArgumentNullException
.ThrowIfNull(action
);
107 TaskCreationOptions options
= TaskCreationOptions
.DenyChildAttach
;
110 options
|= TaskCreationOptions
.LongRunning
| TaskCreationOptions
.PreferFairness
;
113 await Task
.Factory
.StartNew(action
, CancellationToken
.None
, options
, TaskScheduler
.Default
).ConfigureAwait(false);
117 public static void InBackground
<T
>(Func
<T
> function
, bool longRunning
= false) {
118 ArgumentNullException
.ThrowIfNull(function
);
120 InBackground(void () => function(), longRunning
);
124 public static async Task
<IList
<T
>> InParallel
<T
>(IEnumerable
<Task
<T
>> tasks
) {
125 ArgumentNullException
.ThrowIfNull(tasks
);
127 switch (ASF
.GlobalConfig
?.OptimizationMode
) {
128 case GlobalConfig
.EOptimizationMode
.MinMemoryUsage
:
129 List
<T
> results
= [];
131 foreach (Task
<T
> task
in tasks
) {
132 results
.Add(await task
.ConfigureAwait(false));
137 return await Task
.WhenAll(tasks
).ConfigureAwait(false);
142 public static async Task
InParallel(IEnumerable
<Task
> tasks
) {
143 ArgumentNullException
.ThrowIfNull(tasks
);
145 switch (ASF
.GlobalConfig
?.OptimizationMode
) {
146 case GlobalConfig
.EOptimizationMode
.MinMemoryUsage
:
147 foreach (Task task
in tasks
) {
148 await task
.ConfigureAwait(false);
153 await Task
.WhenAll(tasks
).ConfigureAwait(false);
160 public static bool IsClientErrorCode(this HttpStatusCode statusCode
) => statusCode
is >= HttpStatusCode
.BadRequest and
< HttpStatusCode
.InternalServerError
;
163 public static bool IsRedirectionCode(this HttpStatusCode statusCode
) => statusCode
is >= HttpStatusCode
.Ambiguous and
< HttpStatusCode
.BadRequest
;
166 public static bool IsServerErrorCode(this HttpStatusCode statusCode
) => statusCode
is >= HttpStatusCode
.InternalServerError and
< (HttpStatusCode
) 600;
169 public static bool IsSuccessCode(this HttpStatusCode statusCode
) => statusCode
is >= HttpStatusCode
.OK and
< HttpStatusCode
.Ambiguous
;
172 public static bool IsValidCdKey(string key
) {
173 ArgumentException
.ThrowIfNullOrEmpty(key
);
175 return GeneratedRegexes
.CdKey().IsMatch(key
);
179 public static bool IsValidHexadecimalText(string text
) {
180 ArgumentException
.ThrowIfNullOrEmpty(text
);
182 return (text
.Length
% 2 == 0) && text
.All(Uri
.IsHexDigit
);
186 public static IList
<INode
> SelectNodes(this IDocument document
, string xpath
) {
187 ArgumentNullException
.ThrowIfNull(document
);
189 return document
.Body
.SelectNodes(xpath
);
193 public static IEnumerable
<T
> SelectNodes
<T
>(this IDocument document
, string xpath
) where T
: class, INode
{
194 ArgumentNullException
.ThrowIfNull(document
);
196 return document
.Body
.SelectNodes(xpath
).OfType
<T
>();
200 public static IEnumerable
<T
> SelectNodes
<T
>(this IElement element
, string xpath
) where T
: class, INode
{
201 ArgumentNullException
.ThrowIfNull(element
);
203 return element
.SelectNodes(xpath
).OfType
<T
>();
207 public static INode
? SelectSingleNode(this IDocument document
, string xpath
) {
208 ArgumentNullException
.ThrowIfNull(document
);
210 return document
.Body
.SelectSingleNode(xpath
);
214 public static T
? SelectSingleNode
<T
>(this IDocument document
, string xpath
) where T
: class, INode
{
215 ArgumentNullException
.ThrowIfNull(document
);
217 return document
.Body
.SelectSingleNode(xpath
) as T
;
221 public static T
? SelectSingleNode
<T
>(this IElement element
, string xpath
) where T
: class, INode
{
222 ArgumentNullException
.ThrowIfNull(element
);
224 return element
.SelectSingleNode(xpath
) as T
;
228 public static IEnumerable
<T
> ToEnumerable
<T
>(this T item
) {
233 public static string ToHumanReadable(this TimeSpan timeSpan
) => timeSpan
.Humanize(3, maxUnit
: TimeUnit
.Year
, minUnit
: TimeUnit
.Second
);
236 public static Task
<T
> ToLongRunningTask
<T
>(this AsyncJob
<T
> job
) where T
: CallbackMsg
{
237 ArgumentNullException
.ThrowIfNull(job
);
239 job
.Timeout
= TimeSpan
.FromSeconds(TimeoutForLongRunningTasksInSeconds
);
245 public static Task
<AsyncJobMultiple
<T
>.ResultSet
> ToLongRunningTask
<T
>(this AsyncJobMultiple
<T
> job
) where T
: CallbackMsg
{
246 ArgumentNullException
.ThrowIfNull(job
);
248 job
.Timeout
= TimeSpan
.FromSeconds(TimeoutForLongRunningTasksInSeconds
);
254 public static bool TryReadJsonWebToken(string token
, [NotNullWhen(true)] out JsonWebToken
? result
) {
255 ArgumentException
.ThrowIfNullOrEmpty(token
);
258 result
= new JsonWebToken(token
);
259 } catch (Exception e
) {
260 ASF
.ArchiLogger
.LogGenericDebuggingException(e
);
270 internal static ulong MathAdd(ulong first
, int second
) {
272 return first
+ (uint) second
;
275 return first
- (uint) -second
;
278 internal static void OnProgressChanged(string fileName
, byte progressPercentage
) {
279 ArgumentException
.ThrowIfNullOrEmpty(fileName
);
280 ArgumentOutOfRangeException
.ThrowIfGreaterThan(progressPercentage
, 100);
282 const byte printEveryPercentage
= 10;
284 if (progressPercentage
% printEveryPercentage
!= 0) {
288 ASF
.ArchiLogger
.LogGenericDebug($"{fileName} {progressPercentage}%...");
291 internal static async Task
<bool> UpdateCleanup(string targetDirectory
) {
292 ArgumentException
.ThrowIfNullOrEmpty(targetDirectory
);
294 bool updateCleanup
= false;
297 string updateDirectory
= Path
.Combine(targetDirectory
, SharedInfo
.UpdateDirectoryNew
);
299 if (Directory
.Exists(updateDirectory
)) {
300 if (!updateCleanup
) {
301 updateCleanup
= true;
303 ASF
.ArchiLogger
.LogGenericInfo(Strings
.UpdateCleanup
);
306 Directory
.Delete(updateDirectory
, true);
309 string backupDirectory
= Path
.Combine(targetDirectory
, SharedInfo
.UpdateDirectoryOld
);
311 if (Directory
.Exists(backupDirectory
)) {
312 if (!updateCleanup
) {
313 updateCleanup
= true;
315 ASF
.ArchiLogger
.LogGenericInfo(Strings
.UpdateCleanup
);
318 await DeletePotentiallyUsedDirectory(backupDirectory
).ConfigureAwait(false);
320 } catch (Exception e
) {
321 ASF
.ArchiLogger
.LogGenericException(e
);
327 ASF
.ArchiLogger
.LogGenericInfo(Strings
.Done
);
333 internal static async Task
<bool> UpdateFromArchive(ZipArchive zipArchive
, string targetDirectory
) {
334 ArgumentNullException
.ThrowIfNull(zipArchive
);
335 ArgumentException
.ThrowIfNullOrEmpty(targetDirectory
);
337 // Firstly, ensure once again our directories are purged and ready to work with
338 if (!await UpdateCleanup(targetDirectory
).ConfigureAwait(false)) {
342 // Now extract the zip file to entirely new location, this decreases chance of corruptions if user kills the process during this stage
343 string updateDirectory
= Path
.Combine(targetDirectory
, SharedInfo
.UpdateDirectoryNew
);
345 zipArchive
.ExtractToDirectory(updateDirectory
, true);
347 // Now, critical section begins, we're going to move all files from target directory to a backup directory
348 string backupDirectory
= Path
.Combine(targetDirectory
, SharedInfo
.UpdateDirectoryOld
);
350 Directory
.CreateDirectory(backupDirectory
);
352 MoveAllUpdateFiles(targetDirectory
, backupDirectory
);
354 // Finally, we can move the newly extracted files to target directory
355 MoveAllUpdateFiles(updateDirectory
, targetDirectory
, backupDirectory
);
357 // Critical section has finished, we can now cleanup the update directory, backup directory must wait for the process restart
358 Directory
.Delete(updateDirectory
, true);
360 // The update process is done
364 internal static void WarnAboutIncompleteTranslation(ResourceManager resourceManager
) {
365 ArgumentNullException
.ThrowIfNull(resourceManager
);
367 // Skip translation progress for English and invariant (such as "C") cultures
368 switch (CultureInfo
.CurrentUICulture
.TwoLetterISOLanguageName
) {
369 case "en" or
"iv" or
"qps":
373 // We can't dispose this resource set, as we can't be sure if it isn't used somewhere else, rely on GC in this case
374 ResourceSet
? defaultResourceSet
= resourceManager
.GetResourceSet(CultureInfo
.GetCultureInfo("en-US"), true, true);
376 if (defaultResourceSet
== null) {
377 ASF
.ArchiLogger
.LogNullError(defaultResourceSet
);
382 HashSet
<DictionaryEntry
> defaultStringObjects
= defaultResourceSet
.Cast
<DictionaryEntry
>().ToHashSet();
384 if (defaultStringObjects
.Count
== 0) {
385 // This means we don't have entries for English, so there is nothing to check against
386 // Can happen e.g. for plugins with no strings declared which are calling this function
390 // We can't dispose this resource set, as we can't be sure if it isn't used somewhere else, rely on GC in this case
391 ResourceSet
? currentResourceSet
= resourceManager
.GetResourceSet(CultureInfo
.CurrentUICulture
, true, true);
393 if (currentResourceSet
== null) {
394 ASF
.ArchiLogger
.LogNullError(currentResourceSet
);
399 HashSet
<DictionaryEntry
> currentStringObjects
= currentResourceSet
.Cast
<DictionaryEntry
>().ToHashSet();
401 if (currentStringObjects
.Count
>= defaultStringObjects
.Count
) {
402 // Either we have 100% finished translation, or we're missing it entirely and using en-US
403 HashSet
<DictionaryEntry
> testStringObjects
= currentStringObjects
.ToHashSet();
404 testStringObjects
.ExceptWith(defaultStringObjects
);
406 // If we got 0 as final result, this is the missing language
407 // Otherwise it's just a small amount of strings that happen to be the same
408 if (testStringObjects
.Count
== 0) {
409 currentStringObjects
= testStringObjects
;
413 if (currentStringObjects
.Count
< defaultStringObjects
.Count
) {
414 float translationCompleteness
= currentStringObjects
.Count
/ (float) defaultStringObjects
.Count
;
415 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.TranslationIncomplete
, $"{CultureInfo.CurrentUICulture.Name} ({CultureInfo.CurrentUICulture.EnglishName})", translationCompleteness
.ToString("P1", CultureInfo
.CurrentCulture
)));
419 private static async Task
DeletePotentiallyUsedDirectory(string directory
) {
420 ArgumentException
.ThrowIfNullOrEmpty(directory
);
422 for (byte i
= 1; (i
<= MaxSharingViolationTries
) && Directory
.Exists(directory
); i
++) {
424 await Task
.Delay(1000).ConfigureAwait(false);
428 Directory
.Delete(directory
, true);
429 } catch (IOException e
) when ((i
< MaxSharingViolationTries
) && ((uint) e
.HResult
== SharingViolationHResult
)) {
430 // It's entirely possible that old process is still running, we allow this to happen and add additional delay
431 ASF
.ArchiLogger
.LogGenericDebuggingException(e
);
434 } catch (UnauthorizedAccessException e
) when ((i
< MaxSharingViolationTries
) && ((uint) e
.HResult
== UnauthorizedAccessHResult
)) {
435 // It's entirely possible that old process is still running, we allow this to happen and add additional delay
436 ASF
.ArchiLogger
.LogGenericDebuggingException(e
);
445 private static void MoveAllUpdateFiles(string sourceDirectory
, string targetDirectory
, string? backupDirectory
= null) {
446 ArgumentException
.ThrowIfNullOrEmpty(sourceDirectory
);
447 ArgumentException
.ThrowIfNullOrEmpty(targetDirectory
);
449 // Determine if targetDirectory is within sourceDirectory, if yes we need to skip it from enumeration further below
450 string targetRelativeDirectoryPath
= Path
.GetRelativePath(sourceDirectory
, targetDirectory
);
452 // We keep user files if backup directory is null, as it means we're creating one
453 bool keepUserFiles
= string.IsNullOrEmpty(backupDirectory
);
455 foreach (string file
in Directory
.EnumerateFiles(sourceDirectory
, "*", SearchOption
.AllDirectories
)) {
456 string fileName
= Path
.GetFileName(file
);
458 if (string.IsNullOrEmpty(fileName
)) {
459 throw new InvalidOperationException(nameof(fileName
));
462 string relativeFilePath
= Path
.GetRelativePath(sourceDirectory
, file
);
464 if (string.IsNullOrEmpty(relativeFilePath
)) {
465 throw new InvalidOperationException(nameof(relativeFilePath
));
468 string? relativeDirectoryName
= Path
.GetDirectoryName(relativeFilePath
);
470 switch (relativeDirectoryName
) {
472 throw new InvalidOperationException(nameof(relativeDirectoryName
));
474 // No directory, root folder
476 case Logging
.NLogConfigurationFile when keepUserFiles
:
477 case SharedInfo
.LogFile when keepUserFiles
:
478 // Files with those names in root directory we want to keep
483 case SharedInfo
.ArchivalLogsDirectory when keepUserFiles
:
484 case SharedInfo
.ConfigDirectory when keepUserFiles
:
485 case SharedInfo
.DebugDirectory when keepUserFiles
:
486 case SharedInfo
.PluginsDirectory when keepUserFiles
:
487 case SharedInfo
.UpdateDirectoryNew
:
488 case SharedInfo
.UpdateDirectoryOld
:
489 // Files in those constant directories we want to keep in their current place
492 // If we're moving files deeper into source location, we need to skip the newly created location from it
493 if (!string.IsNullOrEmpty(targetRelativeDirectoryPath
) && ((relativeDirectoryName
== targetRelativeDirectoryPath
) || RelativeDirectoryStartsWith(relativeDirectoryName
, targetRelativeDirectoryPath
))) {
497 // Below code block should match the case above, it handles subdirectories
498 if (RelativeDirectoryStartsWith(relativeDirectoryName
, SharedInfo
.UpdateDirectoryNew
, SharedInfo
.UpdateDirectoryOld
)) {
502 if (keepUserFiles
&& RelativeDirectoryStartsWith(relativeDirectoryName
, SharedInfo
.ArchivalLogsDirectory
, SharedInfo
.ConfigDirectory
, SharedInfo
.DebugDirectory
, SharedInfo
.PluginsDirectory
)) {
509 // We're going to move this file out of the current place, overwriting existing one if needed
510 string targetUpdateDirectory
;
512 if (relativeDirectoryName
.Length
> 0) {
513 // File inside a subdirectory
514 targetUpdateDirectory
= Path
.Combine(targetDirectory
, relativeDirectoryName
);
516 Directory
.CreateDirectory(targetUpdateDirectory
);
518 // File in root directory
519 targetUpdateDirectory
= targetDirectory
;
522 string targetUpdateFile
= Path
.Combine(targetUpdateDirectory
, fileName
);
524 // If target update file exists and we have a backup directory, we should consider moving it to the backup directory regardless whether or not we did that before as part of backup procedure
525 // This achieves two purposes, firstly, we ensure additional backup of user file in case something goes wrong, and secondly, we decrease a possibility of overwriting files that are in-use on Windows, since we move them out of the picture first
526 if (!string.IsNullOrEmpty(backupDirectory
) && File
.Exists(targetUpdateFile
)) {
527 string targetBackupDirectory
;
529 if (relativeDirectoryName
.Length
> 0) {
530 // File inside a subdirectory
531 targetBackupDirectory
= Path
.Combine(backupDirectory
, relativeDirectoryName
);
533 Directory
.CreateDirectory(targetBackupDirectory
);
535 // File in root directory
536 targetBackupDirectory
= backupDirectory
;
539 string targetBackupFile
= Path
.Combine(targetBackupDirectory
, fileName
);
541 File
.Move(targetUpdateFile
, targetBackupFile
, true);
544 File
.Move(file
, targetUpdateFile
, true);
548 private static bool RelativeDirectoryStartsWith(string directory
, params string[] prefixes
) {
549 ArgumentException
.ThrowIfNullOrEmpty(directory
);
551 if ((prefixes
== null) || (prefixes
.Length
== 0)) {
552 throw new ArgumentNullException(nameof(prefixes
));
555 return prefixes
.Any(prefix
=> !string.IsNullOrEmpty(prefix
) && (directory
.Length
> prefix
.Length
) && DirectorySeparators
.Contains(directory
[prefix
.Length
]) && directory
.StartsWith(prefix
, StringComparison
.Ordinal
));