Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 76e9413

Browse files
committed
Update Unix SafeHandle to throw NotFound correctly (#11757)
* Update Unix SafeHandle to throw NotFound correctly Need to match Windows semantics for missing files. This means throwing FileNotFound only if the last segment of the path can't be found. * Dispose handle and trim ending separator properly
1 parent d303139 commit 76e9413

File tree

3 files changed

+59
-11
lines changed

3 files changed

+59
-11
lines changed

src/mscorlib/shared/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Diagnostics;
7+
using System.IO;
78
using System.Runtime.InteropServices;
89

910
namespace Microsoft.Win32.SafeHandles
@@ -38,18 +39,30 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : this(ownsHand
3839
internal static SafeFileHandle Open(string path, Interop.Sys.OpenFlags flags, int mode)
3940
{
4041
Debug.Assert(path != null);
42+
SafeFileHandle handle = Interop.Sys.Open(path, flags, mode);
4143

42-
// If we fail to open the file due to a path not existing, we need to know whether to blame
43-
// the file itself or its directory. If we're creating the file, then we blame the directory,
44-
// otherwise we blame the file.
45-
bool enoentDueToDirectory = (flags & Interop.Sys.OpenFlags.O_CREAT) != 0;
46-
47-
// Open the file.
48-
SafeFileHandle handle = Interop.CheckIo(
49-
Interop.Sys.Open(path, flags, mode),
50-
path,
51-
isDirectory: enoentDueToDirectory,
52-
errorRewriter: e => (e.Error == Interop.Error.EISDIR) ? Interop.Error.EACCES.Info() : e);
44+
if (handle.IsInvalid)
45+
{
46+
handle.Dispose();
47+
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
48+
49+
// If we fail to open the file due to a path not existing, we need to know whether to blame
50+
// the file itself or its directory. If we're creating the file, then we blame the directory,
51+
// otherwise we blame the file.
52+
//
53+
// When opening, we need to align with Windows, which considers a missing path to be
54+
// FileNotFound only if the containing directory exists.
55+
56+
bool isDirectory = (error.Error == Interop.Error.ENOENT) &&
57+
((flags & Interop.Sys.OpenFlags.O_CREAT) != 0
58+
|| !DirectoryExists(Path.GetDirectoryName(PathInternal.TrimEndingDirectorySeparator(path))));
59+
60+
Interop.CheckIo(
61+
error.Error,
62+
path,
63+
isDirectory,
64+
errorRewriter: e => (e.Error == Interop.Error.EISDIR) ? Interop.Error.EACCES.Info() : e);
65+
}
5366

5467
// Make sure it's not a directory; we do this after opening it once we have a file descriptor
5568
// to avoid race conditions.
@@ -68,6 +81,31 @@ internal static SafeFileHandle Open(string path, Interop.Sys.OpenFlags flags, in
6881
return handle;
6982
}
7083

84+
private static bool DirectoryExists(string fullPath)
85+
{
86+
int fileType = Interop.Sys.FileTypes.S_IFDIR;
87+
88+
Interop.Sys.FileStatus fileinfo;
89+
Interop.ErrorInfo errorInfo = default(Interop.ErrorInfo);
90+
91+
// First use stat, as we want to follow symlinks. If that fails, it could be because the symlink
92+
// is broken, we don't have permissions, etc., in which case fall back to using LStat to evaluate
93+
// based on the symlink itself.
94+
if (Interop.Sys.Stat(fullPath, out fileinfo) < 0 &&
95+
Interop.Sys.LStat(fullPath, out fileinfo) < 0)
96+
{
97+
errorInfo = Interop.Sys.GetLastErrorInfo();
98+
return false;
99+
}
100+
101+
// Something exists at this path. If the caller is asking for a directory, return true if it's
102+
// a directory and false for everything else. If the caller is asking for a file, return false for
103+
// a directory and true for everything else.
104+
return
105+
(fileType == Interop.Sys.FileTypes.S_IFDIR) ==
106+
((fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR);
107+
}
108+
71109
/// <summary>Opens a SafeFileHandle for a file descriptor created by a provided delegate.</summary>
72110
/// <param name="fdFunc">
73111
/// The function that creates the file descriptor. Returns the file descriptor on success, or an invalid

src/mscorlib/shared/System/IO/PathInternal.Unix.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,10 @@ internal static bool IsPartiallyQualified(string path)
100100
// As long as the path is rooted in Unix it doesn't use the current directory and therefore is fully qualified.
101101
return !Path.IsPathRooted(path);
102102
}
103+
104+
internal static string TrimEndingDirectorySeparator(string path) =>
105+
path.Length > 1 && IsDirectorySeparator(path[path.Length - 1]) ? // exclude root "/"
106+
path.Substring(0, path.Length - 1) :
107+
path;
103108
}
104109
}

src/mscorlib/shared/System/IO/PathInternal.Windows.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,5 +438,10 @@ internal static bool IsDirectoryOrVolumeSeparator(char ch)
438438
{
439439
return IsDirectorySeparator(ch) || VolumeSeparatorChar == ch;
440440
}
441+
442+
internal static string TrimEndingDirectorySeparator(string path) =>
443+
EndsInDirectorySeparator(path) ?
444+
path.Substring(0, path.Length - 1) :
445+
path;
441446
}
442447
}

0 commit comments

Comments
 (0)