Commit f6a6cdbba3

Qusai Hroub <qusai.hroub@outlook.com>
2023-03-04 12:10:33
std.fs.Dir.makeOpenPath: optimize when path already exists
Uses a single NtCreateFile syscall on windows. Closes #12474. Thanks to @joedavis and @matu3ba.
1 parent 1bbe521
Changed files (2)
lib
lib/std/fs/test.zig
@@ -663,6 +663,18 @@ test "file operations on directories" {
     }.impl);
 }
 
+test "makeOpenPath parent dirs do not exist" {
+    var tmp_dir = tmpDir(.{});
+    defer tmp_dir.cleanup();
+
+    var dir = try tmp_dir.dir.makeOpenPath("root_dir/parent_dir/some_dir", .{});
+    dir.close();
+
+    // double check that the full directory structure was created
+    var dir_verification = try tmp_dir.dir.openDir("root_dir/parent_dir/some_dir", .{});
+    dir_verification.close();
+}
+
 test "deleteDir" {
     try testWithAllSupportedPathTypes(struct {
         fn impl(ctx: *TestContext) !void {
lib/std/fs.zig
@@ -1483,22 +1483,91 @@ pub const Dir = struct {
         }
     }
 
+    /// Calls makeOpenDirAccessMaskW recursively to make an entire path
+    /// (i.e. falling back if the parent directory does not exist). Opens the dir if the path
+    /// already exists and is a directory.
+    /// This function is not atomic, and if it returns an error, the file system may
+    /// have been modified regardless.
+    fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) OpenError!Dir {
+        const w = os.windows;
+        var end_index: usize = sub_path.len;
+
+        return while (true) {
+            const sub_path_w = try w.sliceToPrefixedFileW(sub_path[0..end_index]);
+            const result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{
+                .no_follow = no_follow,
+                .create_disposition = if (end_index == sub_path.len) w.FILE_OPEN_IF else w.FILE_CREATE,
+            }) catch |err| switch (err) {
+                error.FileNotFound => {
+                    // march end_index backward until next path component
+                    while (true) {
+                        if (end_index == 0) return err;
+                        end_index -= 1;
+                        if (path.isSep(sub_path[end_index])) break;
+                    }
+                    continue;
+                },
+                else => return err,
+            };
+
+            if (end_index == sub_path.len) return result;
+            // march end_index forward until next path component
+            while (true) {
+                end_index += 1;
+                if (end_index == sub_path.len or path.isSep(sub_path[end_index])) break;
+            }
+        };
+    }
+
     /// This function performs `makePath`, followed by `openDir`.
     /// If supported by the OS, this operation is atomic. It is not atomic on
     /// all operating systems.
+    /// On Windows, this function performs `makeOpenPathAccessMaskW`.
     pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !Dir {
-        // TODO improve this implementation on Windows; we can avoid 1 call to NtClose
-        try self.makePath(sub_path);
-        return self.openDir(sub_path, open_dir_options);
+        return switch (builtin.os.tag) {
+            .windows => {
+                const w = os.windows;
+                const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
+                    w.SYNCHRONIZE | w.FILE_TRAVERSE;
+
+                return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow);
+            },
+            else => {
+                return self.openDir(sub_path, open_dir_options) catch |err| switch (err) {
+                    error.FileNotFound => {
+                        try self.makePath(sub_path);
+                        return self.openDir(sub_path, open_dir_options);
+                    },
+                    else => |e| return e,
+                };
+            },
+        };
     }
 
     /// This function performs `makePath`, followed by `openIterableDir`.
     /// If supported by the OS, this operation is atomic. It is not atomic on
     /// all operating systems.
     pub fn makeOpenPathIterable(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !IterableDir {
-        // TODO improve this implementation on Windows; we can avoid 1 call to NtClose
-        try self.makePath(sub_path);
-        return self.openIterableDir(sub_path, open_dir_options);
+        return switch (builtin.os.tag) {
+            .windows => {
+                const w = os.windows;
+                const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
+                    w.SYNCHRONIZE | w.FILE_TRAVERSE | w.FILE_LIST_DIRECTORY;
+
+                return IterableDir{
+                    .dir = try self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow),
+                };
+            },
+            else => {
+                return self.openIterableDir(sub_path, open_dir_options) catch |err| switch (err) {
+                    error.FileNotFound => {
+                        try self.makePath(sub_path);
+                        return self.openIterableDir(sub_path, open_dir_options);
+                    },
+                    else => |e| return e,
+                };
+            },
+        };
     }
 
     ///  This function returns the canonicalized absolute pathname of
@@ -1742,7 +1811,10 @@ pub const Dir = struct {
         const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
             w.SYNCHRONIZE | w.FILE_TRAVERSE;
         const flags: u32 = if (iterable) base_flags | w.FILE_LIST_DIRECTORY else base_flags;
-        var dir = try self.openDirAccessMaskW(sub_path_w, flags, args.no_follow);
+        var dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{
+            .no_follow = args.no_follow,
+            .create_disposition = w.FILE_OPEN,
+        });
         return dir;
     }
 
@@ -1765,7 +1837,12 @@ pub const Dir = struct {
         return Dir{ .fd = fd };
     }
 
-    fn openDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, no_follow: bool) OpenError!Dir {
+    const MakeOpenDirAccessMaskWOptions = struct {
+        no_follow: bool,
+        create_disposition: u32,
+    };
+
+    fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) OpenError!Dir {
         const w = os.windows;
 
         var result = Dir{
@@ -1786,7 +1863,7 @@ pub const Dir = struct {
             .SecurityDescriptor = null,
             .SecurityQualityOfService = null,
         };
-        const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0;
+        const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0;
         var io: w.IO_STATUS_BLOCK = undefined;
         const rc = w.ntdll.NtCreateFile(
             &result.fd,
@@ -1794,13 +1871,14 @@ pub const Dir = struct {
             &attr,
             &io,
             null,
-            0,
+            w.FILE_ATTRIBUTE_NORMAL,
             w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
-            w.FILE_OPEN,
+            flags.create_disposition,
             w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point,
             null,
             0,
         );
+
         switch (rc) {
             .SUCCESS => return result,
             .OBJECT_NAME_INVALID => return error.BadPathName,