From 37bf1453c734da29606c066f3158743609f577d4 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Thu, 18 Nov 2021 10:22:54 +0100 Subject: [PATCH] CreateDirectory: eliminate some syscalls. (#58799) --- .../src/System/IO/FileSystem.Unix.cs | 156 ++++++++++-------- 1 file changed, 86 insertions(+), 70 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs index ea2165d04308c..e23ae9e9e28a8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs @@ -277,101 +277,117 @@ public static void DeleteFile(string fullPath) public static void CreateDirectory(string fullPath) { - // NOTE: This logic is primarily just carried forward from Win32FileSystem.CreateDirectory. + // The argument is a full path, which means it is an absolute path that + // doesn't contain "//", "/./", and "/../". + Debug.Assert(fullPath.Length > 0); + Debug.Assert(PathInternal.IsDirectorySeparator(fullPath[0])); - int length = fullPath.Length; - - // We need to trim the trailing slash or the code will try to create 2 directories of the same name. - if (length >= 2 && Path.EndsInDirectorySeparator(fullPath)) + if (fullPath.Length == 1) { - length--; + return; // fullPath is '/'. } - // For paths that are only // or /// - if (length == 2 && PathInternal.IsDirectorySeparator(fullPath[1])) + int result = Interop.Sys.MkDir(fullPath, (int)Interop.Sys.Permissions.Mask); + if (result == 0) { - throw new IOException(SR.Format(SR.IO_CannotCreateDirectory, fullPath)); + return; // Created directory. } - // We can save a bunch of work if the directory we want to create already exists. - if (DirectoryExists(fullPath)) + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error == Interop.Error.EEXIST && DirectoryExists(fullPath)) { - return; + return; // Path already exists and it's a directory. } + else if (errorInfo.Error == Interop.Error.ENOENT) + { + // Some parts of the path don't exist yet. + CreateParentsAndDirectory(fullPath); + } + else + { + throw Interop.GetExceptionForIoErrno(errorInfo, fullPath, isDirectory: true); + } + } - // Attempt to figure out which directories don't exist, and only create the ones we need. - bool somepathexists = false; + public static void CreateParentsAndDirectory(string fullPath) + { + // Try create parents bottom to top and track those that could not + // be created due to missing parents. Then create them top to bottom. List stackDir = new List(); - int lengthRoot = PathInternal.GetRootLength(fullPath); - if (length > lengthRoot) + + stackDir.Add(fullPath); + + int i = fullPath.Length - 1; + // Trim trailing separator. + if (PathInternal.IsDirectorySeparator(fullPath[i])) + { + i--; + } + do { - int i = length - 1; - while (i >= lengthRoot && !somepathexists) + // Find the end of the parent directory. + Debug.Assert(!PathInternal.IsDirectorySeparator(fullPath[i])); + while (!PathInternal.IsDirectorySeparator(fullPath[i])) { - if (!DirectoryExists(fullPath.AsSpan(0, i + 1))) // Create only the ones missing - { - stackDir.Add(fullPath.Substring(0, i + 1)); - } - else - { - somepathexists = true; - } - - while (i > lengthRoot && !PathInternal.IsDirectorySeparator(fullPath[i])) - { - i--; - } i--; } - } - int count = stackDir.Count; - if (count == 0 && !somepathexists) - { - ReadOnlySpan root = Path.GetPathRoot(fullPath.AsSpan()); - if (!DirectoryExists(root)) + // Try create it. + string mkdirPath = fullPath.Substring(0, i); + int result = Interop.Sys.MkDir(mkdirPath, (int)Interop.Sys.Permissions.Mask); + if (result == 0) { - throw Interop.GetExceptionForIoErrno(Interop.Error.ENOENT.Info(), fullPath, isDirectory: true); + // Created parent. + break; } - return; - } - // Create all the directories - int result = 0; - Interop.ErrorInfo firstError = default(Interop.ErrorInfo); - string errorString = fullPath; - for (int i = stackDir.Count - 1; i >= 0; i--) - { - string name = stackDir[i]; + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error == Interop.Error.ENOENT) + { + // Some parts of the path don't exist yet. + // We'll try to create its parent on the next iteration. - // The mkdir command uses 0777 by default (it'll be AND'd with the process umask internally). - // We do the same. - result = Interop.Sys.MkDir(name, (int)Interop.Sys.Permissions.Mask); - if (result < 0 && firstError.Error == 0) + // Track this path for later creation. + stackDir.Add(mkdirPath); + } + else if (errorInfo.Error == Interop.Error.EEXIST) { - Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + // Parent exists. + // If it is not a directory, MkDir will fail when we create a child directory. + break; + } + else + { + throw Interop.GetExceptionForIoErrno(errorInfo, mkdirPath, isDirectory: true); + } + i--; + } while (i > 0); - // While we tried to avoid creating directories that don't - // exist above, there are a few cases that can fail, e.g. - // a race condition where another process or thread creates - // the directory first, or there's a file at the location. - if (errorInfo.Error != Interop.Error.EEXIST) - { - firstError = errorInfo; - } - else if (FileExists(name) || (!DirectoryExists(name, out errorInfo) && errorInfo.Error == Interop.Error.EACCES)) + // Create directories that had missing parents. + for (i = stackDir.Count - 1; i >= 0; i--) + { + string mkdirPath = stackDir[i]; + int result = Interop.Sys.MkDir(mkdirPath, (int)Interop.Sys.Permissions.Mask); + if (result < 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error == Interop.Error.EEXIST) { - // If there's a file in this directory's place, or if we have ERROR_ACCESS_DENIED when checking if the directory already exists throw. - firstError = errorInfo; - errorString = name; + // Path was created since we last checked. + // Continue, and for the last item, which is fullPath, + // verify it is actually a directory. + if (i != 0) + { + continue; + } + if (DirectoryExists(mkdirPath)) + { + return; + } } - } - } - // Only throw an exception if creating the exact directory we wanted failed to work correctly. - if (result < 0 && firstError.Error != 0) - { - throw Interop.GetExceptionForIoErrno(firstError, errorString, isDirectory: true); + throw Interop.GetExceptionForIoErrno(errorInfo, mkdirPath, isDirectory: true); + } } }