Ampr: fix path case on Linux (#750)

This commit is contained in:
999sian
2026-08-02 22:36:00 +01:00
committed by GitHub
parent f36ce4084a
commit 8df4039ca4
7 changed files with 144 additions and 33 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ public static class AmprExports
private const int MaxCachedHostFiles = 1536;
private static readonly object _hostFileCacheGate = new();
private static readonly Dictionary<string, LinkedListNode<CachedHostFileEntry>> _hostFileByPath =
new(StringComparer.OrdinalIgnoreCase);
new(HostFsPath.Comparer);
private static readonly LinkedList<CachedHostFileEntry> _hostFileLru = new();
[SysAbiExport(
+33 -16
View File
@@ -111,7 +111,7 @@ internal static class AmprFileRegistry
{
while (true)
{
if (string.Equals(_indexedApp0Root, normalizedRoot, StringComparison.OrdinalIgnoreCase))
if (string.Equals(_indexedApp0Root, normalizedRoot, HostFsPath.Comparison))
{
return;
}
@@ -123,7 +123,7 @@ internal static class AmprFileRegistry
if (string.Equals(
_indexingApp0Root,
normalizedRoot,
StringComparison.OrdinalIgnoreCase))
HostFsPath.Comparison))
{
Monitor.Wait(_indexGate);
continue;
@@ -174,20 +174,34 @@ internal static class AmprFileRegistry
}
var relatives = new List<string>(256 * 1024);
foreach (var hostPath in Directory.EnumerateFiles(
normalizedRoot,
"*",
SearchOption.AllDirectories))
try
{
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
.Replace('\\', '/');
if (string.IsNullOrEmpty(relative) ||
relative.StartsWith("..", StringComparison.Ordinal))
foreach (var hostPath in Directory.EnumerateFiles(
normalizedRoot,
"*",
SearchOption.AllDirectories))
{
continue;
}
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
.Replace('\\', '/');
if (string.IsNullOrEmpty(relative) ||
relative.StartsWith("..", StringComparison.Ordinal))
{
continue;
}
relatives.Add(relative);
relatives.Add(relative);
}
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
// The walk is an opportunistic warm-up reached synchronously from
// sceAmprCommandBufferConstructor; a dump that moves or a mount
// that hiccups must not fault the guest export. The background
// preload already swallows this. Leave the root unindexed so a
// later call retries.
Console.Error.WriteLine(
$"[LOADER][WARN] ampr.app0_index_walk_failed root={normalizedRoot}: {exception.Message}");
return;
}
// Hash + dictionary fill dominates under Rosetta once the walk is
@@ -315,7 +329,10 @@ internal static class AmprFileRegistry
"ampr-index");
Directory.CreateDirectory(cacheDir);
var rootHash = ComputeFileId(normalizedRoot.ToLowerInvariant());
// Distinct roots must not share a cache file. Folding case is only
// correct where the host filesystem folds it too.
var rootKey = OperatingSystem.IsWindows() ? normalizedRoot.ToLowerInvariant() : normalizedRoot;
var rootHash = ComputeFileId(rootKey);
return Path.Combine(cacheDir, $"app0-{rootHash:x8}.v{version}.idx");
}
@@ -360,7 +377,7 @@ internal static class AmprFileRegistry
}
var root = reader.ReadString();
if (!string.Equals(root, normalizedRoot, StringComparison.OrdinalIgnoreCase))
if (!string.Equals(root, normalizedRoot, HostFsPath.Comparison))
{
return false;
}
@@ -470,7 +487,7 @@ internal static class AmprFileRegistry
return;
}
var relatives = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var relatives = new HashSet<string>(HostFsPath.Comparer);
foreach (var hostPath in _hostPathsById.Values)
{
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
+21
View File
@@ -0,0 +1,21 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs;
/// <summary>
/// Key equivalence for caches and comparisons over <em>host</em> filesystem
/// paths. Windows resolves names case-insensitively, but Linux hosts are
/// case-sensitive and the guest filesystem is too, so a dump can legitimately
/// contain "DATA.BIN" alongside "Data.bin". An ignore-case cache aliases those
/// distinct files into one entry there, which silently serves the wrong bytes
/// or drops one of them entirely.
/// </summary>
internal static class HostFsPath
{
public static readonly StringComparer Comparer =
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
public static readonly StringComparison Comparison =
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
}
@@ -117,17 +117,12 @@ public static partial class KernelMemoryCompatExports
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
// Both caches memoize host filesystem probe outcomes, so their key
// equivalence must match the host filesystem's: Windows resolves names
// case-insensitively, but Linux hosts are case-sensitive, and an
// ignore-case cache there aliases distinct paths — a cached miss for
// "/app0/DATA.BIN" keeps answering NOT_FOUND for "/app0/Data.bin" even
// though that file exists and a fresh probe would find it.
private static readonly StringComparer HostFsPathComparer =
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
private static readonly StringComparison HostFsPathComparison =
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
private static readonly HashSet<string> _negativeStatCache = new(HostFsPathComparer);
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(HostFsPathComparer);
// equivalence must match the host filesystem's — see HostFsPath. On a
// case-sensitive host an ignore-case cache aliases distinct paths: a
// cached miss for "/app0/DATA.BIN" keeps answering NOT_FOUND for
// "/app0/Data.bin" even though that file exists.
private static readonly HashSet<string> _negativeStatCache = new(HostFsPath.Comparer);
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(HostFsPath.Comparer);
private static long _nextFileDescriptor = 2;
private static string _applicationTitleId = "UNKNOWN";
@@ -5203,8 +5198,8 @@ public static partial class KernelMemoryCompatExports
// host would let a relative path escape into a sibling directory that
// differs from the mount root only by case (root ".../Save" vs
// sibling ".../save").
if (!string.Equals(candidate, matchedHostRoot, HostFsPathComparison) &&
!candidate.StartsWith(rootWithSeparator, HostFsPathComparison))
if (!string.Equals(candidate, matchedHostRoot, HostFsPath.Comparison) &&
!candidate.StartsWith(rootWithSeparator, HostFsPath.Comparison))
{
return false;
}
@@ -5305,8 +5300,8 @@ public static partial class KernelMemoryCompatExports
var rootWithSeparator =
Path.TrimEndingDirectorySeparator(fullRoot) + Path.DirectorySeparatorChar;
if (!string.Equals(candidate, fullRoot, HostFsPathComparison) &&
!candidate.StartsWith(rootWithSeparator, HostFsPathComparison))
if (!string.Equals(candidate, fullRoot, HostFsPath.Comparison) &&
!candidate.StartsWith(rootWithSeparator, HostFsPath.Comparison))
{
return string.Empty;
}
@@ -5332,7 +5327,7 @@ public static partial class KernelMemoryCompatExports
private static bool EscapesMountViaReparsePoint(string mountRoot, string candidate)
{
var rootTrimmed = Path.TrimEndingDirectorySeparator(mountRoot);
if (string.Equals(candidate, rootTrimmed, HostFsPathComparison))
if (string.Equals(candidate, rootTrimmed, HostFsPath.Comparison))
{
return false;
}