fix(launcher): harden updater crash recovery

This commit is contained in:
Erik 2026-08-14 23:12:15 +02:00
parent 2d2a5b5046
commit 1955ca8ab5
27 changed files with 2714 additions and 544 deletions

View file

@ -503,7 +503,9 @@ public sealed class ClientVersionStore
.Where(path => !string.Equals(
path,
"install.json",
StringComparison.OrdinalIgnoreCase))
OperatingSystem.IsWindows()
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal))
.OrderBy(path => path, StringComparer.Ordinal)
.ToArray();
string[] recordedFiles = record.Files
@ -809,13 +811,43 @@ public sealed class ClientVersionStore
".client-staging-*",
SearchOption.TopDirectoryOnly))
{
string suffix = Path.GetFileName(path)[".client-staging-".Length..];
if (Guid.TryParseExact(suffix, "N", out _))
if (HasCanonicalGuidName(
Path.GetFileName(path),
".client-staging-",
string.Empty))
{
SafeZipExtractor.TryDeleteDirectory(path);
}
}
foreach (string path in Directory.EnumerateDirectories(
AppDirectory,
".client-corrupt-*",
SearchOption.TopDirectoryOnly))
{
if (HasCanonicalGuidName(
Path.GetFileName(path),
".client-corrupt-",
string.Empty))
{
SafeZipExtractor.TryDeleteDirectory(path);
}
}
foreach (string path in Directory.EnumerateFiles(
AppDirectory,
".client-download-*.zip",
SearchOption.TopDirectoryOnly))
{
if (HasCanonicalGuidName(
Path.GetFileName(path),
".client-download-",
".zip"))
{
VerifiedArtifactDownloader.TryDelete(path);
}
}
foreach (string path in Directory.EnumerateFiles(
AppDirectory,
".current*.tmp",
@ -825,20 +857,43 @@ public sealed class ClientVersionStore
string[] parts = fileName.Split('.');
if (parts.Length >= 4
&& string.Equals(parts[^1], "tmp", StringComparison.Ordinal)
&& Guid.TryParseExact(parts[^2], "N", out _))
&& Guid.TryParseExact(parts[^2], "N", out Guid parsed)
&& string.Equals(
parsed.ToString("N"),
parts[^2],
StringComparison.Ordinal))
{
VerifiedArtifactDownloader.TryDelete(path);
}
}
}
private static bool HasCanonicalGuidName(
string fileName,
string prefix,
string suffix)
{
if (!fileName.StartsWith(prefix, StringComparison.Ordinal)
|| !fileName.EndsWith(suffix, StringComparison.Ordinal)
|| fileName.Length != prefix.Length + 32 + suffix.Length)
{
return false;
}
string value = fileName.Substring(prefix.Length, 32);
return Guid.TryParseExact(value, "N", out Guid parsed)
&& string.Equals(parsed.ToString("N"), value, StringComparison.Ordinal);
}
private void RequireOwnedStagingPath(string path)
{
string parent = Path.GetDirectoryName(path) ?? string.Empty;
string fileName = Path.GetFileName(path);
if (!PathsEqual(parent, AppDirectory)
|| !fileName.StartsWith(".client-staging-", StringComparison.Ordinal)
|| !Guid.TryParseExact(fileName[".client-staging-".Length..], "N", out _))
|| !HasCanonicalGuidName(
fileName,
".client-staging-",
string.Empty))
{
throw new LauncherUpdateException(
"The client extraction path is not an owned LA10 staging directory.");
@ -901,7 +956,7 @@ public sealed class ClientVersionStore
&& !part.Any(character =>
char.IsControl(character)
|| character is '<' or '>' or '"' or '|' or '?' or '*')
&& !IsWindowsDeviceName(part));
&& !PortablePathRules.IsWindowsDeviceName(part));
}
internal static string ResolveContained(string root, string relative)
@ -929,19 +984,6 @@ public sealed class ClientVersionStore
return path;
}
private static bool IsWindowsDeviceName(string segment)
{
string stem = segment.Split('.')[0];
return stem.Equals("CON", StringComparison.OrdinalIgnoreCase)
|| stem.Equals("PRN", StringComparison.OrdinalIgnoreCase)
|| stem.Equals("AUX", StringComparison.OrdinalIgnoreCase)
|| stem.Equals("NUL", StringComparison.OrdinalIgnoreCase)
|| (stem.Length == 4
&& (stem.StartsWith("COM", StringComparison.OrdinalIgnoreCase)
|| stem.StartsWith("LPT", StringComparison.OrdinalIgnoreCase))
&& stem[3] is >= '1' and <= '9');
}
private static bool PathsEqual(string left, string right) =>
string.Equals(
Path.TrimEndingDirectorySeparator(Path.GetFullPath(left)),