diff --git a/Robust.Server/ServerStatus/StatusHost.Acz.Sources.cs b/Robust.Server/ServerStatus/StatusHost.Acz.Sources.cs index afa4fdb0ac0..a2a264763f2 100644 --- a/Robust.Server/ServerStatus/StatusHost.Acz.Sources.cs +++ b/Robust.Server/ServerStatus/StatusHost.Acz.Sources.cs @@ -105,11 +105,11 @@ internal sealed partial class StatusHost private Task SourceAczDictionaryViaFile(AssetPass pass, IPackageLogger logger) { var path = PathHelpers.ExecutableRelativeFile("Content.Client.zip"); - if (!File.Exists(path)) + if (!FileHelper.TryOpenFileRead(path, out var fileStream)) return Task.FromResult(false); _aczSawmill.Info($"StatusHost found client zip: {path}"); - using var zip = new ZipArchive(File.OpenRead(path), ZipArchiveMode.Read, leaveOpen: false); + using var zip = new ZipArchive(fileStream, ZipArchiveMode.Read, leaveOpen: false); SourceAczDictionaryViaZipStream(zip, pass, logger); return Task.FromResult(true); } diff --git a/Robust.Shared/ContentPack/AssemblyTypeChecker.cs b/Robust.Shared/ContentPack/AssemblyTypeChecker.cs index d9715814661..bca94209581 100644 --- a/Robust.Shared/ContentPack/AssemblyTypeChecker.cs +++ b/Robust.Shared/ContentPack/AssemblyTypeChecker.cs @@ -895,12 +895,10 @@ public Resolver(AssemblyTypeChecker parent, string[] diskLoadPaths, ResPath[] re { var path = Path.Combine(diskLoadPath, dllName); - if (!File.Exists(path)) - { + if (!FileHelper.TryOpenFileRead(path, out var fileStream)) continue; - } - return ModLoader.MakePEReader(File.OpenRead(path)); + return ModLoader.MakePEReader(fileStream); } foreach (var resLoadPath in _resLoadPaths) diff --git a/Robust.Shared/ContentPack/DirLoader.cs b/Robust.Shared/ContentPack/DirLoader.cs index 215d2a1cafb..2e5cb5f8d66 100644 --- a/Robust.Shared/ContentPack/DirLoader.cs +++ b/Robust.Shared/ContentPack/DirLoader.cs @@ -46,16 +46,11 @@ public void Mount() public bool TryGetFile(ResPath relPath, [NotNullWhen(true)] out Stream? stream) { var path = GetPath(relPath); - if (!File.Exists(path)) - { - stream = null; - return false; - } - CheckPathCasing(relPath); - stream = File.OpenRead(path); - return true; + var ret = FileHelper.TryOpenFileRead(path, out var fStream); + stream = fStream; + return ret; } public bool FileExists(ResPath relPath) diff --git a/Robust.Shared/Utility/FileHelper.cs b/Robust.Shared/Utility/FileHelper.cs new file mode 100644 index 00000000000..e4dcfe8c822 --- /dev/null +++ b/Robust.Shared/Utility/FileHelper.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +using TerraFX.Interop.Windows; + +namespace Robust.Shared.Utility; + +internal static class FileHelper +{ + /// + /// Try to open a file for reading. If the file does not exist, the operation fails without exception. + /// + /// + /// This API is not atomic and can thus be vulnerable to TOCTOU attacks. Don't use it if that's relevant. + /// + /// The path to try to open. + /// The resulting file stream. + /// True if the file existed and was opened. + public static bool TryOpenFileRead(string path, [NotNullWhen(true)] out FileStream? stream) + { + // On Windows, the separate File.Exists() call alone adds a ton of weight. + // The alternative however (opening the file and catching the error) is extremely slow because of .NET exceptions. + // So we manually call the windows API and make the file handle from that. Problem solved! + if (OperatingSystem.IsWindows()) + return TryGetFileWindows(path, out stream); + + if (!File.Exists(path)) + { + stream = null; + return false; + } + + stream = File.OpenRead(path); + return true; + } + + private static unsafe bool TryGetFileWindows(string path, [NotNullWhen(true)] out FileStream? stream) + { + HANDLE file; + fixed (char* pPath = path) + { + file = Windows.CreateFileW( + (ushort*)pPath, + Windows.GENERIC_READ, + FILE.FILE_SHARE_READ, + null, + OPEN.OPEN_EXISTING, + FILE.FILE_ATTRIBUTE_NORMAL, + HANDLE.NULL); + } + + if (file == HANDLE.INVALID_VALUE) + { + var lastError = Marshal.GetLastWin32Error(); + if (lastError is ERROR.ERROR_FILE_NOT_FOUND or ERROR.ERROR_PATH_NOT_FOUND) + { + stream = null; + return false; + } + + Marshal.ThrowExceptionForHR(Windows.HRESULT_FROM_WIN32(lastError)); + } + + var sf = new SafeFileHandle(file, ownsHandle: true); + stream = new FileStream(sf, FileAccess.Read); + return true; + } +}