Skip to content
Merged
55 changes: 29 additions & 26 deletions src/libraries/System.IO.FileSystem/tests/Directory/Exists.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class Directory_Exists : FileSystemTest
{
#region Utilities

public bool Exists(string path)
public virtual bool Exists(string path)
{
return Directory.Exists(path);
}
Expand Down Expand Up @@ -57,17 +57,6 @@ public void PathWithInvalidCharactersAsPath_ReturnsFalse(string invalidPath)
Assert.False(Exists(TestDirectory + Path.DirectorySeparatorChar + invalidPath));
}

[Fact]
public void PathAlreadyExistsAsFile()
{
string path = GetTestFilePath();
File.Create(path).Dispose();

Assert.False(Exists(IOServices.RemoveTrailingSlash(path)));
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
}

[Fact]
public void PathAlreadyExistsAsDirectory()
{
Expand Down Expand Up @@ -180,18 +169,6 @@ public void ValidExtendedPathExists_ReturnsTrue(string component)
Assert.True(Exists(path));
}

[ConditionalFact(nameof(UsingNewNormalization))]
[PlatformSpecific(TestPlatforms.Windows)] // Extended path already exists as file
public void ExtendedPathAlreadyExistsAsFile()
{
string path = IOInputs.ExtendedPrefix + GetTestFilePath();
File.Create(path).Dispose();

Assert.False(Exists(IOServices.RemoveTrailingSlash(path)));
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
}

[ConditionalFact(nameof(UsingNewNormalization))]
[PlatformSpecific(TestPlatforms.Windows)] // Extended path already exists as directory
public void ExtendedPathAlreadyExistsAsDirectory()
Expand Down Expand Up @@ -389,6 +366,34 @@ public void SubdirectoryOnNonExistentDriveAsPath_ReturnsFalse()
Assert.False(Exists(Path.Combine(IOServices.GetNonExistentDrive(), "nonexistentsubdir")));
}

#endregion
}

public class Directory_ExistsAsFile : FileSystemTest
{
[Fact]
public void PathAlreadyExistsAsFile()
{
string path = GetTestFilePath();
File.Create(path).Dispose();

Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(path)));
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
}

[ConditionalFact(nameof(UsingNewNormalization))]
[PlatformSpecific(TestPlatforms.Windows)] // Extended path already exists as file
public void ExtendedPathAlreadyExistsAsFile()
{
string path = IOInputs.ExtendedPrefix + GetTestFilePath();
File.Create(path).Dispose();

Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(path)));
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
Assert.False(Directory.Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
}

[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] // Makes call to native code (libc)
public void FalseForNonRegularFile()
Expand All @@ -397,7 +402,5 @@ public void FalseForNonRegularFile()
Assert.Equal(0, mkfifo(fileName, 0));
Assert.False(Directory.Exists(fileName));
}

#endregion
}
}
39 changes: 23 additions & 16 deletions src/libraries/System.IO.FileSystem/tests/File/Exists.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ public void PathWithInvalidCharactersAsPath_ReturnsFalse(string invalidPath)
{
// Checks that errors aren't thrown when calling Exists() on paths with impossible to create characters
Assert.False(Exists(invalidPath));

Assert.False(Exists(".."));
Assert.False(Exists("."));
}

[Fact]
Expand Down Expand Up @@ -100,17 +97,6 @@ public void PathEndsInAltTrailingSlash_AndExists_Windows()
Assert.False(Exists(path + Path.DirectorySeparatorChar));
}

[Fact]
public void PathAlreadyExistsAsDirectory()
{
string path = GetTestFilePath();
Directory.CreateDirectory(path);

Assert.False(Exists(IOServices.RemoveTrailingSlash(path)));
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
}

[Fact]
public void DirectoryLongerThanMaxDirectoryAsPath_DoesntThrow()
{
Expand Down Expand Up @@ -246,6 +232,29 @@ public void DirectoryWithComponentLongerThanMaxComponentAsPath_ReturnsFalse(stri
Assert.False(Exists(component));
}

#endregion
}

public class File_ExistsAsDirectory : FileSystemTest
{
[Fact]
public void DotAsPathReturnsFalse()
{
Assert.False(File.Exists("."));
Assert.False(File.Exists(".."));
}

[Fact]
public void PathAlreadyExistsAsDirectory()
{
string path = GetTestFilePath();
Directory.CreateDirectory(path);

Assert.False(File.Exists(IOServices.RemoveTrailingSlash(path)));
Assert.False(File.Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
Assert.False(File.Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
}

[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] // Uses P/Invokes
public void FalseForNonRegularFile()
Expand All @@ -254,7 +263,5 @@ public void FalseForNonRegularFile()
Assert.Equal(0, mkfifo(fileName, 0));
Assert.True(File.Exists(fileName));
}

#endregion
}
}
35 changes: 35 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/Path/Exists_Directory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;

namespace System.IO.Tests
{
public class PathDirectory_Exists : Directory_Exists
{
public override bool Exists(string path) => Path.Exists(path);

[Fact]
public void PathAlreadyExistsAsFile()
{
string path = GetTestFilePath();
File.Create(path).Dispose();

Assert.True(Exists(IOServices.RemoveTrailingSlash(path)));
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
}

[ConditionalFact(nameof(UsingNewNormalization))]
[PlatformSpecific(TestPlatforms.Windows)] // Extended path already exists as file
public void ExtendedPathAlreadyExistsAsFile()
{
string path = IOInputs.ExtendedPrefix + GetTestFilePath();
File.Create(path).Dispose();

Assert.True(Exists(IOServices.RemoveTrailingSlash(path)));
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
}
}
}
32 changes: 32 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/Path/Exists_File.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;

namespace System.IO.Tests
{
public class PathFile_Exists : File_Exists
{
public override bool Exists(string path) => Path.Exists(path);

[Fact]
public void PathAlreadyExistsAsDirectory()
{
string path = GetTestFilePath();
Directory.CreateDirectory(path);

Assert.True(Exists(IOServices.RemoveTrailingSlash(path)));
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
}

[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] // Uses P/Invokes
public void TrueForNonRegularFile()
{
string fileName = GetTestFilePath();
Assert.Equal(0, mkfifo(fileName, 0));
Assert.True(Exists(fileName));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
<Compile Include="Enumeration\SymbolicLinksTests.cs" />
<Compile Include="LargeFileTests.cs" />
<Compile Include="PathInternalTests.cs" />
<Compile Include="Path\Exists_Directory.cs" />
<Compile Include="Path\Exists_File.cs" />
<Compile Include="RandomAccess\Base.cs" />
<Compile Include="RandomAccess\GetLength.cs" />
<Compile Include="RandomAccess\Read.cs" />
Expand Down
16 changes: 16 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/IO/Path.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ public static partial class Path

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

// Checks if the given path is available for use.
private static bool ExistsCore(string fullPath)
{
bool result = Interop.Sys.LStat(fullPath, out Interop.Sys.FileStatus fileInfo) == Interop.Errors.ERROR_SUCCESS;
if (PathInternal.IsDirectorySeparator(fullPath[fullPath.Length - 1]))
{
// If the path ends with trailing slash, we want to make sure it's a directory.
// Although Lstat returns the correct result on desktop platforms,
// on browser WASM, it seems to strip trailing slash, leading to false positives for files.
// This check prevents it from doing so.
result = result && (fileInfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
}

return result;
}

// Expands the given path to a fully qualified path.
public static string GetFullPath(string path)
{
Expand Down
17 changes: 17 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/IO/Path.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ public static partial class Path
(char)31
};

private static bool ExistsCore(string fullPath)
{
Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default;
int errorCode = FileSystem.FillAttributeInfo(fullPath, ref data, returnErrorOnNotFound: true);
bool result = (errorCode == Interop.Errors.ERROR_SUCCESS) && (data.dwFileAttributes != -1);

if (PathInternal.IsDirectorySeparator(fullPath[fullPath.Length - 1]))
{
// We want to make sure that if the path ends in a trailing slash, it's truly a directory
// because FillAttributeInfo syscall removes any trailing slashes and may give false positives
// for existing files.
result = result && (data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0;
}

return result;
}

// Expands the given path to a fully qualified path.
public static string GetFullPath(string path)
{
Expand Down
35 changes: 35 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/IO/Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,41 @@ public static partial class Path
string.Concat(subpath, ".", extension);
}

/// <summary>
/// Determines whether the specified file or directory exists.
/// </summary>
/// <remarks>
/// Unlike <see cref="File.Exists(string?)"/> it returns true for existing, non-regular files like pipes.
/// If the path targets an existing link, but the target of the link does not exist, it returns true.
/// </remarks>
/// <param name="path">The path to check</param>
/// <returns>
/// <see langword="true" /> if the caller has the required permissions and <paramref name="path" /> contains
/// the name of an existing file or directory; otherwise, <see langword="false" />.
/// This method also returns <see langword="false" /> if <paramref name="path" /> is <see langword="null" />,
/// an invalid path, or a zero-length string. If the caller does not have sufficient permissions to read the specified path,
/// no exception is thrown and the method returns <see langword="false" /> regardless of the existence of <paramref name="path" />.
/// </returns>
public static bool Exists([NotNullWhen(true)] string? path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}

string? fullPath;
try
{
fullPath = GetFullPath(path);
}
catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException)
{
return false;
}

return ExistsCore(fullPath);
}

/// <summary>
/// Returns the directory portion of a file path. This method effectively
/// removes the last segment of the given file path, i.e. it returns a
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10724,6 +10724,7 @@ public static partial class Path
public static string Combine(params string[] paths) { throw null; }
public static bool EndsInDirectorySeparator(System.ReadOnlySpan<char> path) { throw null; }
public static bool EndsInDirectorySeparator(string path) { throw null; }
public static bool Exists([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? path) { throw null; }
public static System.ReadOnlySpan<char> GetDirectoryName(System.ReadOnlySpan<char> path) { throw null; }
public static string? GetDirectoryName(string? path) { throw null; }
public static System.ReadOnlySpan<char> GetExtension(System.ReadOnlySpan<char> path) { throw null; }
Expand Down