Skip to content

Commit c712129

Browse files
Tarun047adamsitnik
andauthored
Add Path.Exists to replace inefficient File.Exists || Directory.Exists (#64347)
Fixes #21678 Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
1 parent bc6d349 commit c712129

9 files changed

Lines changed: 190 additions & 42 deletions

File tree

src/libraries/System.IO.FileSystem/tests/Directory/Exists.cs

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class Directory_Exists : FileSystemTest
1010
{
1111
#region Utilities
1212

13-
public bool Exists(string path)
13+
public virtual bool Exists(string path)
1414
{
1515
return Directory.Exists(path);
1616
}
@@ -57,17 +57,6 @@ public void PathWithInvalidCharactersAsPath_ReturnsFalse(string invalidPath)
5757
Assert.False(Exists(TestDirectory + Path.DirectorySeparatorChar + invalidPath));
5858
}
5959

60-
[Fact]
61-
public void PathAlreadyExistsAsFile()
62-
{
63-
string path = GetTestFilePath();
64-
File.Create(path).Dispose();
65-
66-
Assert.False(Exists(IOServices.RemoveTrailingSlash(path)));
67-
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
68-
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
69-
}
70-
7160
[Fact]
7261
public void PathAlreadyExistsAsDirectory()
7362
{
@@ -180,18 +169,6 @@ public void ValidExtendedPathExists_ReturnsTrue(string component)
180169
Assert.True(Exists(path));
181170
}
182171

183-
[ConditionalFact(nameof(UsingNewNormalization))]
184-
[PlatformSpecific(TestPlatforms.Windows)] // Extended path already exists as file
185-
public void ExtendedPathAlreadyExistsAsFile()
186-
{
187-
string path = IOInputs.ExtendedPrefix + GetTestFilePath();
188-
File.Create(path).Dispose();
189-
190-
Assert.False(Exists(IOServices.RemoveTrailingSlash(path)));
191-
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
192-
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
193-
}
194-
195172
[ConditionalFact(nameof(UsingNewNormalization))]
196173
[PlatformSpecific(TestPlatforms.Windows)] // Extended path already exists as directory
197174
public void ExtendedPathAlreadyExistsAsDirectory()
@@ -387,6 +364,34 @@ public void SubdirectoryOnNonExistentDriveAsPath_ReturnsFalse()
387364
Assert.False(Exists(Path.Combine(IOServices.GetNonExistentDrive(), "nonexistentsubdir")));
388365
}
389366

367+
#endregion
368+
}
369+
370+
public class Directory_ExistsAsFile : FileSystemTest
371+
{
372+
[Fact]
373+
public void PathAlreadyExistsAsFile()
374+
{
375+
string path = GetTestFilePath();
376+
File.Create(path).Dispose();
377+
378+
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(path)));
379+
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
380+
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
381+
}
382+
383+
[ConditionalFact(nameof(UsingNewNormalization))]
384+
[PlatformSpecific(TestPlatforms.Windows)] // Extended path already exists as file
385+
public void ExtendedPathAlreadyExistsAsFile()
386+
{
387+
string path = IOInputs.ExtendedPrefix + GetTestFilePath();
388+
File.Create(path).Dispose();
389+
390+
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(path)));
391+
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
392+
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
393+
}
394+
390395
[Fact]
391396
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] // Makes call to native code (libc)
392397
public void FalseForNonRegularFile()
@@ -395,7 +400,5 @@ public void FalseForNonRegularFile()
395400
Assert.Equal(0, mkfifo(fileName, 0));
396401
Assert.False(Directory.Exists(fileName));
397402
}
398-
399-
#endregion
400403
}
401404
}

src/libraries/System.IO.FileSystem/tests/File/Exists.cs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ public void PathWithInvalidCharactersAsPath_ReturnsFalse(string invalidPath)
5252
{
5353
// Checks that errors aren't thrown when calling Exists() on paths with impossible to create characters
5454
Assert.False(Exists(invalidPath));
55-
56-
Assert.False(Exists(".."));
57-
Assert.False(Exists("."));
5855
}
5956

6057
[Fact]
@@ -100,17 +97,6 @@ public void PathEndsInAltTrailingSlash_AndExists_Windows()
10097
Assert.False(Exists(path + Path.AltDirectorySeparatorChar));
10198
}
10299

103-
[Fact]
104-
public void PathAlreadyExistsAsDirectory()
105-
{
106-
string path = GetTestFilePath();
107-
Directory.CreateDirectory(path);
108-
109-
Assert.False(Exists(IOServices.RemoveTrailingSlash(path)));
110-
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
111-
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
112-
}
113-
114100
[Fact]
115101
public void DirectoryLongerThanMaxDirectoryAsPath_DoesntThrow()
116102
{
@@ -244,6 +230,29 @@ public void DirectoryWithComponentLongerThanMaxComponentAsPath_ReturnsFalse(stri
244230
Assert.False(Exists(component));
245231
}
246232

233+
#endregion
234+
}
235+
236+
public class File_ExistsAsDirectory : FileSystemTest
237+
{
238+
[Fact]
239+
public void DotAsPathReturnsFalse()
240+
{
241+
Assert.False(File.Exists("."));
242+
Assert.False(File.Exists(".."));
243+
}
244+
245+
[Fact]
246+
public void PathAlreadyExistsAsDirectory()
247+
{
248+
string path = GetTestFilePath();
249+
Directory.CreateDirectory(path);
250+
251+
Assert.False(File.Exists(IOServices.RemoveTrailingSlash(path)));
252+
Assert.False(File.Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
253+
Assert.False(File.Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
254+
}
255+
247256
[Fact]
248257
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] // Uses P/Invokes
249258
public void FalseForNonRegularFile()
@@ -252,7 +261,5 @@ public void FalseForNonRegularFile()
252261
Assert.Equal(0, mkfifo(fileName, 0));
253262
Assert.True(File.Exists(fileName));
254263
}
255-
256-
#endregion
257264
}
258265
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Xunit;
5+
6+
namespace System.IO.Tests
7+
{
8+
public class PathDirectory_Exists : Directory_Exists
9+
{
10+
public override bool Exists(string path) => Path.Exists(path);
11+
12+
[Fact]
13+
public void PathAlreadyExistsAsFile()
14+
{
15+
string path = GetTestFilePath();
16+
File.Create(path).Dispose();
17+
18+
Assert.True(Exists(IOServices.RemoveTrailingSlash(path)));
19+
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
20+
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
21+
}
22+
23+
[ConditionalFact(nameof(UsingNewNormalization))]
24+
[PlatformSpecific(TestPlatforms.Windows)] // Extended path already exists as file
25+
public void ExtendedPathAlreadyExistsAsFile()
26+
{
27+
string path = IOInputs.ExtendedPrefix + GetTestFilePath();
28+
File.Create(path).Dispose();
29+
30+
Assert.True(Exists(IOServices.RemoveTrailingSlash(path)));
31+
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
32+
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
33+
}
34+
}
35+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Xunit;
5+
6+
namespace System.IO.Tests
7+
{
8+
public class PathFile_Exists : File_Exists
9+
{
10+
public override bool Exists(string path) => Path.Exists(path);
11+
12+
[Fact]
13+
public void PathAlreadyExistsAsDirectory()
14+
{
15+
string path = GetTestFilePath();
16+
Directory.CreateDirectory(path);
17+
18+
Assert.True(Exists(IOServices.RemoveTrailingSlash(path)));
19+
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
20+
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
21+
}
22+
23+
[Fact]
24+
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] // Uses P/Invokes
25+
public void TrueForNonRegularFile()
26+
{
27+
string fileName = GetTestFilePath();
28+
Assert.Equal(0, mkfifo(fileName, 0));
29+
Assert.True(Exists(fileName));
30+
}
31+
}
32+
}

src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
<Compile Include="Enumeration\SymbolicLinksTests.cs" />
5959
<Compile Include="LargeFileTests.cs" />
6060
<Compile Include="PathInternalTests.cs" />
61+
<Compile Include="Path\Exists_Directory.cs" />
62+
<Compile Include="Path\Exists_File.cs" />
6163
<Compile Include="RandomAccess\Base.cs" />
6264
<Compile Include="RandomAccess\GetLength.cs" />
6365
<Compile Include="RandomAccess\Read.cs" />

src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ public static partial class Path
1313

1414
public static char[] GetInvalidPathChars() => new char[] { '\0' };
1515

16+
// Checks if the given path is available for use.
17+
private static bool ExistsCore(string fullPath)
18+
{
19+
bool result = Interop.Sys.LStat(fullPath, out Interop.Sys.FileStatus fileInfo) == Interop.Errors.ERROR_SUCCESS;
20+
if (PathInternal.IsDirectorySeparator(fullPath[fullPath.Length - 1]))
21+
{
22+
// If the path ends with trailing slash, we want to make sure it's a directory.
23+
// Although Lstat returns the correct result on desktop platforms,
24+
// on browser WASM, it seems to strip trailing slash, leading to false positives for files.
25+
// This check prevents it from doing so.
26+
result = result && (fileInfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
27+
}
28+
29+
return result;
30+
}
31+
1632
// Expands the given path to a fully qualified path.
1733
public static string GetFullPath(string path)
1834
{

src/libraries/System.Private.CoreLib/src/System/IO/Path.Windows.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ public static partial class Path
2727
(char)31
2828
};
2929

30+
private static bool ExistsCore(string fullPath)
31+
{
32+
Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default;
33+
int errorCode = FileSystem.FillAttributeInfo(fullPath, ref data, returnErrorOnNotFound: true);
34+
bool result = (errorCode == Interop.Errors.ERROR_SUCCESS) && (data.dwFileAttributes != -1);
35+
36+
if (PathInternal.IsDirectorySeparator(fullPath[fullPath.Length - 1]))
37+
{
38+
// We want to make sure that if the path ends in a trailing slash, it's truly a directory
39+
// because FillAttributeInfo syscall removes any trailing slashes and may give false positives
40+
// for existing files.
41+
result = result && (data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0;
42+
}
43+
44+
return result;
45+
}
46+
3047
// Expands the given path to a fully qualified path.
3148
public static string GetFullPath(string path)
3249
{

src/libraries/System.Private.CoreLib/src/System/IO/Path.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,41 @@ public static partial class Path
7575
string.Concat(subpath, ".", extension);
7676
}
7777

78+
/// <summary>
79+
/// Determines whether the specified file or directory exists.
80+
/// </summary>
81+
/// <remarks>
82+
/// Unlike <see cref="File.Exists(string?)"/> it returns true for existing, non-regular files like pipes.
83+
/// If the path targets an existing link, but the target of the link does not exist, it returns true.
84+
/// </remarks>
85+
/// <param name="path">The path to check</param>
86+
/// <returns>
87+
/// <see langword="true" /> if the caller has the required permissions and <paramref name="path" /> contains
88+
/// the name of an existing file or directory; otherwise, <see langword="false" />.
89+
/// This method also returns <see langword="false" /> if <paramref name="path" /> is <see langword="null" />,
90+
/// an invalid path, or a zero-length string. If the caller does not have sufficient permissions to read the specified path,
91+
/// no exception is thrown and the method returns <see langword="false" /> regardless of the existence of <paramref name="path" />.
92+
/// </returns>
93+
public static bool Exists([NotNullWhen(true)] string? path)
94+
{
95+
if (string.IsNullOrEmpty(path))
96+
{
97+
return false;
98+
}
99+
100+
string? fullPath;
101+
try
102+
{
103+
fullPath = GetFullPath(path);
104+
}
105+
catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException)
106+
{
107+
return false;
108+
}
109+
110+
return ExistsCore(fullPath);
111+
}
112+
78113
/// <summary>
79114
/// Returns the directory portion of a file path. This method effectively
80115
/// removes the last segment of the given file path, i.e. it returns a

src/libraries/System.Runtime/ref/System.Runtime.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10725,6 +10725,7 @@ public static partial class Path
1072510725
public static string Combine(params string[] paths) { throw null; }
1072610726
public static bool EndsInDirectorySeparator(System.ReadOnlySpan<char> path) { throw null; }
1072710727
public static bool EndsInDirectorySeparator(string path) { throw null; }
10728+
public static bool Exists([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? path) { throw null; }
1072810729
public static System.ReadOnlySpan<char> GetDirectoryName(System.ReadOnlySpan<char> path) { throw null; }
1072910730
public static string? GetDirectoryName(string? path) { throw null; }
1073010731
public static System.ReadOnlySpan<char> GetExtension(System.ReadOnlySpan<char> path) { throw null; }

0 commit comments

Comments
 (0)