diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig index 57c1dfb945..5754cc2d53 100644 --- a/lib/std/child_process.zig +++ b/lib/std/child_process.zig @@ -364,6 +364,7 @@ pub const ChildProcess = struct { error.FileTooBig => unreachable, error.DeviceBusy => unreachable, error.FileLocksNotSupported => unreachable, + error.BadPathName => unreachable, // Windows-only else => |e| return e, } else @@ -480,25 +481,20 @@ pub const ChildProcess = struct { const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore); - // TODO use CreateFileW here since we are using a string literal for the path const nul_handle = if (any_ignore) - windows.CreateFile( - "NUL", - windows.GENERIC_READ, - windows.FILE_SHARE_READ, - null, - windows.OPEN_EXISTING, - windows.FILE_ATTRIBUTE_NORMAL, - null, - ) catch |err| switch (err) { - error.SharingViolation => unreachable, // not possible for "NUL" + windows.OpenFile(&[_]u16{ 'N', 'U', 'L' }, .{ + .dir = std.fs.cwd().fd, + .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, + .share_access = windows.FILE_SHARE_READ, + .creation = windows.OPEN_EXISTING, + .io_mode = .blocking, + }) catch |err| switch (err) { error.PathAlreadyExists => unreachable, // not possible for "NUL" error.PipeBusy => unreachable, // not possible for "NUL" - error.InvalidUtf8 => unreachable, // not possible for "NUL" - error.BadPathName => unreachable, // not possible for "NUL" error.FileNotFound => unreachable, // not possible for "NUL" error.AccessDenied => unreachable, // not possible for "NUL" error.NameTooLong => unreachable, // not possible for "NUL" + error.WouldBlock => unreachable, // not possible for "NUL" else => |e| return e, } else diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 6e3a53b772..57c1534e97 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -225,8 +225,7 @@ pub fn makeDirAbsoluteZ(absolute_path_z: [*:0]const u8) !void { /// Same as `makeDirAbsolute` except the parameter is a null-terminated WTF-16 encoded string. pub fn makeDirAbsoluteW(absolute_path_w: [*:0]const u16) !void { assert(path.isAbsoluteWindowsW(absolute_path_w)); - const handle = try os.windows.CreateDirectoryW(null, absolute_path_w, null); - os.windows.CloseHandle(handle); + return os.mkdirW(absolute_path_w, default_new_dir_mode); } pub const deleteDir = @compileError("deprecated; use dir.deleteDir or deleteDirAbsolute"); @@ -881,8 +880,7 @@ pub const Dir = struct { } pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) !void { - const handle = try os.windows.CreateDirectoryW(self.fd, sub_path, null); - os.windows.CloseHandle(handle); + try os.mkdiratW(self.fd, sub_path, default_new_dir_mode); } /// Calls makeDir recursively to make an entire path. Returns success if the path @@ -1119,7 +1117,7 @@ pub const Dir = struct { pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.sliceToPrefixedFileW(sub_path); - return self.deleteFileW(sub_path_w.span().ptr); + return self.deleteFileW(sub_path_w.span()); } else if (builtin.os.tag == .wasi) { os.unlinkatWasi(self.fd, sub_path, 0) catch |err| switch (err) { error.DirNotEmpty => unreachable, // not passing AT_REMOVEDIR @@ -1153,7 +1151,7 @@ pub const Dir = struct { } /// Same as `deleteFile` except the parameter is WTF-16 encoded. - pub fn deleteFileW(self: Dir, sub_path_w: [*:0]const u16) DeleteFileError!void { + pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void { os.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) { error.DirNotEmpty => unreachable, // not passing AT_REMOVEDIR else => |e| return e, @@ -1182,7 +1180,7 @@ pub const Dir = struct { pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.sliceToPrefixedFileW(sub_path); - return self.deleteDirW(sub_path_w.span().ptr); + return self.deleteDirW(sub_path_w.span()); } else if (builtin.os.tag == .wasi) { os.unlinkat(self.fd, sub_path, os.AT_REMOVEDIR) catch |err| switch (err) { error.IsDir => unreachable, // not possible since we pass AT_REMOVEDIR @@ -1204,7 +1202,7 @@ pub const Dir = struct { /// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed. /// This function is Windows-only. - pub fn deleteDirW(self: Dir, sub_path_w: [*:0]const u16) DeleteDirError!void { + pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void { os.unlinkatW(self.fd, sub_path_w, os.AT_REMOVEDIR) catch |err| switch (err) { error.IsDir => unreachable, // not possible since we pass AT_REMOVEDIR else => |e| return e, @@ -1263,11 +1261,11 @@ pub const Dir = struct { /// are null-terminated, WTF16 encoded. pub fn symLinkW( self: Dir, - target_path_w: [:0]const u16, - sym_link_path_w: [:0]const u16, + target_path_w: []const u16, + sym_link_path_w: []const u16, flags: SymLinkFlags, ) !void { - return os.windows.CreateSymbolicLinkW(self.fd, sym_link_path_w, target_path_w, flags.is_directory); + return os.windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory); } /// Read value of a symbolic link. @@ -1278,7 +1276,8 @@ pub const Dir = struct { return self.readLinkWasi(sub_path, buffer); } if (builtin.os.tag == .windows) { - return os.windows.ReadLink(self.fd, sub_path, buffer); + const sub_path_w = try os.windows.sliceToPrefixedFileW(sub_path); + return self.readLinkW(sub_path_w.span(), buffer); } const sub_path_c = try os.toPosixPath(sub_path); return self.readLinkZ(&sub_path_c, buffer); @@ -1295,15 +1294,15 @@ pub const Dir = struct { pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.cStrToPrefixedFileW(sub_path_c); - return self.readLinkW(sub_path_w, buffer); + return self.readLinkW(sub_path_w.span(), buffer); } return os.readlinkatZ(self.fd, sub_path_c, buffer); } /// Windows-only. Same as `readLink` except the pathname parameter /// is null-terminated, WTF16 encoded. - pub fn readLinkW(self: Dir, sub_path_w: [*:0]const u16, buffer: []u8) ![]u8 { - return os.windows.ReadLinkW(self.fd, sub_path_w, buffer); + pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { + return os.windows.ReadLink(self.fd, sub_path_w, buffer); } /// On success, caller owns returned buffer. @@ -1813,7 +1812,9 @@ pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags assert(path.isAbsolute(target_path)); assert(path.isAbsolute(sym_link_path)); if (builtin.os.tag == .windows) { - return os.windows.CreateSymbolicLink(null, sym_link_path, target_path, flags.is_directory); + const target_path_w = try os.windows.sliceToPrefixedFileW(target_path); + const sym_link_path_w = try os.windows.sliceToPrefixedFileW(sym_link_path); + return os.windows.CreateSymbolicLink(null, sym_link_path_w.span(), target_path_w.span(), flags.is_directory); } return os.symlink(target_path, sym_link_path); } @@ -1822,10 +1823,10 @@ pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags /// Note that this function will by default try creating a symbolic link to a file. If you would /// like to create a symbolic link to a directory, specify this with `SymLinkFlags{ .is_directory = true }`. /// See also `symLinkAbsolute`, `symLinkAbsoluteZ`. -pub fn symLinkAbsoluteW(target_path_w: [:0]const u16, sym_link_path_w: [:0]const u16, flags: SymLinkFlags) !void { - assert(path.isAbsoluteWindowsW(target_path_w)); - assert(path.isAbsoluteWindowsW(sym_link_path_w)); - return os.windows.CreateSymbolicLinkW(null, sym_link_path_w, target_path_w, flags.is_directory); +pub fn symLinkAbsoluteW(target_path_w: []const u16, sym_link_path_w: []const u16, flags: SymLinkFlags) !void { + assert(path.isAbsoluteWindowsWTF16(target_path_w)); + assert(path.isAbsoluteWindowsWTF16(sym_link_path_w)); + return os.windows.CreateSymbolicLink(null, sym_link_path_w, target_path_w, flags.is_directory); } /// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. @@ -1836,7 +1837,7 @@ pub fn symLinkAbsoluteZ(target_path_c: [*:0]const u8, sym_link_path_c: [*:0]cons if (builtin.os.tag == .windows) { const target_path_w = try os.windows.cStrToWin32PrefixedFileW(target_path_c); const sym_link_path_w = try os.windows.cStrToWin32PrefixedFileW(sym_link_path_c); - return os.windows.CreateSymbolicLinkW(sym_link_path_w.span().ptr, target_path_w.span().ptr, flags.is_directory); + return os.windows.CreateSymbolicLink(sym_link_path_w.span(), target_path_w.span(), flags.is_directory); } return os.symlinkZ(target_path_c, sym_link_path_c); } @@ -1938,7 +1939,20 @@ pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker { return walker; } -pub const OpenSelfExeError = os.OpenError || os.windows.CreateFileError || SelfExePathError || os.FlockError; +pub const OpenSelfExeError = error{ + SharingViolation, + PathAlreadyExists, + FileNotFound, + AccessDenied, + PipeBusy, + NameTooLong, + /// On Windows, file paths must be valid Unicode. + InvalidUtf8, + /// On Windows, file paths cannot contain these characters: + /// '/', '*', '?', '"', '<', '>', '|' + BadPathName, + Unexpected, +} || os.OpenError || SelfExePathError || os.FlockError; pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File { if (builtin.os.tag == .linux) { diff --git a/lib/std/fs/file.zig b/lib/std/fs/file.zig index cffc8cf87e..3b79e4e01b 100644 --- a/lib/std/fs/file.zig +++ b/lib/std/fs/file.zig @@ -47,7 +47,20 @@ pub const File = struct { else => 0o666, }; - pub const OpenError = windows.CreateFileError || os.OpenError || os.FlockError; + pub const OpenError = error{ + SharingViolation, + PathAlreadyExists, + FileNotFound, + AccessDenied, + PipeBusy, + NameTooLong, + /// On Windows, file paths must be valid Unicode. + InvalidUtf8, + /// On Windows, file paths cannot contain these characters: + /// '/', '*', '?', '"', '<', '>', '|' + BadPathName, + Unexpected, + } || os.OpenError || os.FlockError; pub const Lock = enum { None, Shared, Exclusive }; diff --git a/lib/std/fs/watch.zig b/lib/std/fs/watch.zig index b161e45c71..635818b702 100644 --- a/lib/std/fs/watch.zig +++ b/lib/std/fs/watch.zig @@ -374,15 +374,13 @@ pub fn Watch(comptime V: type) type { defer if (!basename_utf16le_null_consumed) self.allocator.free(basename_utf16le_null); const basename_utf16le_no_null = basename_utf16le_null[0 .. basename_utf16le_null.len - 1]; - const dir_handle = try windows.CreateFileW( - dirname_utf16le.ptr, - windows.FILE_LIST_DIRECTORY, - windows.FILE_SHARE_READ | windows.FILE_SHARE_DELETE | windows.FILE_SHARE_WRITE, - null, - windows.OPEN_EXISTING, - windows.FILE_FLAG_BACKUP_SEMANTICS | windows.FILE_FLAG_OVERLAPPED, - null, - ); + const dir_handle = try windows.OpenFile(dirname_utf16le, .{ + .dir = std.fs.cwd().fd, + .access_mask = windows.FILE_LIST_DIRECTORY, + .creation = windows.FILE_OPEN, + .io_mode = .blocking, + .open_dir = true, + }); var dir_handle_consumed = false; defer if (!dir_handle_consumed) windows.CloseHandle(dir_handle); diff --git a/lib/std/os.zig b/lib/std/os.zig index 1665a2efa6..06b61e8c38 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1041,6 +1041,9 @@ pub const OpenError = error{ /// The underlying filesystem does not support file locks FileLocksNotSupported, + + BadPathName, + InvalidUtf8, } || UnexpectedError; /// Open and possibly create a file. Keeps trying if it gets interrupted. @@ -1092,18 +1095,65 @@ pub fn openZ(file_path: [*:0]const u8, flags: u32, perm: mode_t) OpenError!fd_t } } +fn openOptionsFromFlags(flags: u32) windows.OpenFileOptions { + const w = windows; + + var access_mask: w.ULONG = w.READ_CONTROL | w.FILE_WRITE_ATTRIBUTES | w.SYNCHRONIZE; + if (flags & O_RDWR != 0) { + access_mask |= w.GENERIC_READ | w.GENERIC_WRITE; + } else if (flags & O_WRONLY != 0) { + access_mask |= w.GENERIC_WRITE; + } else { + access_mask |= w.GENERIC_READ | w.GENERIC_WRITE; + } + + const open_dir: bool = flags & O_DIRECTORY != 0; + const follow_symlinks: bool = flags & O_NOFOLLOW == 0; + + const creation: w.ULONG = blk: { + if (flags & O_CREAT != 0) { + if (flags & O_EXCL != 0) { + break :blk w.FILE_CREATE; + } + } + break :blk w.FILE_OPEN; + }; + + return .{ + .access_mask = access_mask, + .io_mode = .blocking, + .creation = creation, + .open_dir = open_dir, + .follow_symlinks = follow_symlinks, + }; +} + /// Windows-only. The path parameter is /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. /// Translates the POSIX open API call to a Windows API call. -pub fn openW(file_path_w: []const u16, flags: u32, perm: usize) OpenError!fd_t { - @compileError("TODO implement openW for windows"); +/// TODO currently, this function does not handle all flag combinations +/// or makes use of perm argument. +pub fn openW(file_path_w: []const u16, flags: u32, perm: mode_t) OpenError!fd_t { + var options = openOptionsFromFlags(flags); + options.dir = std.fs.cwd().fd; + return windows.OpenFile(file_path_w, options) catch |err| switch (err) { + error.WouldBlock => unreachable, + error.PipeBusy => unreachable, + else => |e| return e, + }; } /// Open and possibly create a file. Keeps trying if it gets interrupted. /// `file_path` is relative to the open directory handle `dir_fd`. /// See also `openatC`. -/// TODO support windows pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) OpenError!fd_t { + if (builtin.os.tag == .wasi) { + @compileError("use openatWasi instead"); + } + if (builtin.os.tag == .windows) { + const file_path_w = try windows.sliceToPrefixedFileW(file_path); + return openatW(dir_fd, file_path_w.span(), flags, mode); + } const file_path_c = try toPosixPath(file_path); return openatZ(dir_fd, &file_path_c, flags, mode); } @@ -1145,8 +1195,11 @@ pub const openatC = @compileError("deprecated: renamed to openatZ"); /// Open and possibly create a file. Keeps trying if it gets interrupted. /// `file_path` is relative to the open directory handle `dir_fd`. /// See also `openat`. -/// TODO support windows pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) OpenError!fd_t { + if (builtin.os.tag == .windows) { + const file_path_w = try windows.cStrToPrefixedFileW(file_path); + return openatW(dir_fd, file_path_w.span(), flags, mode); + } while (true) { const rc = system.openat(dir_fd, file_path, flags, mode); switch (errno(rc)) { @@ -1177,6 +1230,20 @@ pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) } } +/// Windows-only. Similar to `openat` but with pathname argument null-terminated +/// WTF16 encoded. +/// TODO currently, this function does not handle all flag combinations +/// or makes use of perm argument. +pub fn openatW(dir_fd: fd_t, file_path_w: []const u16, flags: u32, mode: mode_t) OpenError!fd_t { + var options = openOptionsFromFlags(flags); + options.dir = dir_fd; + return windows.OpenFile(file_path_w, options) catch |err| switch (err) { + error.WouldBlock => unreachable, + error.PipeBusy => unreachable, + else => |e| return e, + }; +} + pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void { while (true) { switch (errno(system.dup2(old_fd, new_fd))) { @@ -1683,7 +1750,7 @@ pub fn unlink(file_path: []const u8) UnlinkError!void { @compileError("unlink is not supported in WASI; use unlinkat instead"); } else if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); - return windows.DeleteFileW(file_path_w.span().ptr); + return unlinkW(file_path_w.span()); } else { const file_path_c = try toPosixPath(file_path); return unlinkZ(&file_path_c); @@ -1696,7 +1763,7 @@ pub const unlinkC = @compileError("deprecated: renamed to unlinkZ"); pub fn unlinkZ(file_path: [*:0]const u8) UnlinkError!void { if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path); - return windows.DeleteFileW(file_path_w.span().ptr); + return unlinkW(file_path_w.span()); } switch (errno(system.unlink(file_path))) { 0 => return, @@ -1717,6 +1784,11 @@ pub fn unlinkZ(file_path: [*:0]const u8) UnlinkError!void { } } +/// Windows-only. Same as `unlink` except the parameter is null-terminated, WTF16 encoded. +pub fn unlinkW(file_path_w: []const u16) UnlinkError!void { + return windows.DeleteFile(file_path_w, .{ .dir = std.fs.cwd().fd }); +} + pub const UnlinkatError = UnlinkError || error{ /// When passing `AT_REMOVEDIR`, this error occurs when the named directory is not empty. DirNotEmpty, @@ -1727,7 +1799,7 @@ pub const UnlinkatError = UnlinkError || error{ pub fn unlinkat(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatError!void { if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); - return unlinkatW(dirfd, file_path_w.span().ptr, flags); + return unlinkatW(dirfd, file_path_w.span(), flags); } else if (builtin.os.tag == .wasi) { return unlinkatWasi(dirfd, file_path, flags); } else { @@ -1774,7 +1846,7 @@ pub fn unlinkatWasi(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatErro pub fn unlinkatZ(dirfd: fd_t, file_path_c: [*:0]const u8, flags: u32) UnlinkatError!void { if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path_c); - return unlinkatW(dirfd, file_path_w.span().ptr, flags); + return unlinkatW(dirfd, file_path_w.span(), flags); } switch (errno(system.unlinkat(dirfd, file_path_c, flags))) { 0 => return, @@ -1800,67 +1872,9 @@ pub fn unlinkatZ(dirfd: fd_t, file_path_c: [*:0]const u8, flags: u32) UnlinkatEr } /// Same as `unlinkat` but `sub_path_w` is UTF16LE, NT prefixed. Windows only. -pub fn unlinkatW(dirfd: fd_t, sub_path_w: [*:0]const u16, flags: u32) UnlinkatError!void { - const w = windows; - - const want_rmdir_behavior = (flags & AT_REMOVEDIR) != 0; - const create_options_flags = if (want_rmdir_behavior) - @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT) - else - @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_NON_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT); // would we ever want to delete the target instead? - - const path_len_bytes = @intCast(u16, mem.lenZ(sub_path_w) * 2); - var nt_name = w.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - // The Windows API makes this mutable, but it will not mutate here. - .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w)), - }; - - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - // Windows does not recognize this, but it does work with empty string. - nt_name.Length = 0; - } - if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { - // Can't remove the parent directory with an open handle. - return error.FileBusy; - } - - var attr = w.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w)) null else dirfd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - var io: w.IO_STATUS_BLOCK = undefined; - var tmp_handle: w.HANDLE = undefined; - var rc = w.ntdll.NtCreateFile( - &tmp_handle, - w.SYNCHRONIZE | w.DELETE, - &attr, - &io, - null, - 0, - w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, - w.FILE_OPEN, - create_options_flags, - null, - 0, - ); - if (rc == .SUCCESS) { - rc = w.ntdll.NtClose(tmp_handle); - } - switch (rc) { - .SUCCESS => return, - .OBJECT_NAME_INVALID => unreachable, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .INVALID_PARAMETER => unreachable, - .FILE_IS_A_DIRECTORY => return error.IsDir, - .NOT_A_DIRECTORY => return error.NotDir, - else => return w.unexpectedStatus(rc), - } +pub fn unlinkatW(dirfd: fd_t, sub_path_w: []const u16, flags: u32) UnlinkatError!void { + const remove_dir = (flags & AT_REMOVEDIR) != 0; + return windows.DeleteFile(sub_path_w, .{ .dir = dirfd, .remove_dir = remove_dir }); } const RenameError = error{ @@ -2087,7 +2101,7 @@ pub fn renameatW( pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: u32) MakeDirError!void { if (builtin.os.tag == .windows) { const sub_dir_path_w = try windows.sliceToPrefixedFileW(sub_dir_path); - return mkdiratW(dir_fd, sub_dir_path_w.span().ptr, mode); + return mkdiratW(dir_fd, sub_dir_path_w.span(), mode); } else if (builtin.os.tag == .wasi) { return mkdiratWasi(dir_fd, sub_dir_path, mode); } else { @@ -2145,8 +2159,19 @@ pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: u32) MakeDirErr } } -pub fn mkdiratW(dir_fd: fd_t, sub_path_w: [*:0]const u16, mode: u32) MakeDirError!void { - const sub_dir_handle = try windows.CreateDirectoryW(dir_fd, sub_path_w, null); +pub fn mkdiratW(dir_fd: fd_t, sub_path_w: []const u16, mode: u32) MakeDirError!void { + const sub_dir_handle = windows.OpenFile(sub_path_w, .{ + .dir = dir_fd, + .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, + .creation = windows.FILE_CREATE, + .io_mode = .blocking, + .open_dir = true, + }) catch |err| switch (err) { + error.IsDir => unreachable, + error.PipeBusy => unreachable, + error.WouldBlock => unreachable, + else => |e| return e, + }; windows.CloseHandle(sub_dir_handle); } @@ -2175,9 +2200,8 @@ pub fn mkdir(dir_path: []const u8, mode: u32) MakeDirError!void { if (builtin.os.tag == .wasi) { @compileError("mkdir is not supported in WASI; use mkdirat instead"); } else if (builtin.os.tag == .windows) { - const sub_dir_handle = try windows.CreateDirectory(null, dir_path, null); - windows.CloseHandle(sub_dir_handle); - return; + const dir_path_w = try windows.sliceToPrefixedFileW(dir_path); + return mkdirW(dir_path_w.span(), mode); } else { const dir_path_c = try toPosixPath(dir_path); return mkdirZ(&dir_path_c, mode); @@ -2188,9 +2212,7 @@ pub fn mkdir(dir_path: []const u8, mode: u32) MakeDirError!void { pub fn mkdirZ(dir_path: [*:0]const u8, mode: u32) MakeDirError!void { if (builtin.os.tag == .windows) { const dir_path_w = try windows.cStrToPrefixedFileW(dir_path); - const sub_dir_handle = try windows.CreateDirectoryW(null, dir_path_w.span().ptr, null); - windows.CloseHandle(sub_dir_handle); - return; + return mkdirW(dir_path_w.span(), mode); } switch (errno(system.mkdir(dir_path, mode))) { 0 => return, @@ -2211,6 +2233,23 @@ pub fn mkdirZ(dir_path: [*:0]const u8, mode: u32) MakeDirError!void { } } +/// Windows-only. Same as `mkdir` but the parameters is WTF16 encoded. +pub fn mkdirW(dir_path_w: []const u16, mode: u32) MakeDirError!void { + const sub_dir_handle = windows.OpenFile(dir_path_w, .{ + .dir = std.fs.cwd().fd, + .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, + .creation = windows.FILE_CREATE, + .io_mode = .blocking, + .open_dir = true, + }) catch |err| switch (err) { + error.IsDir => unreachable, + error.PipeBusy => unreachable, + error.WouldBlock => unreachable, + else => |e| return e, + }; + windows.CloseHandle(sub_dir_handle); +} + pub const DeleteDirError = error{ AccessDenied, FileBusy, @@ -2231,7 +2270,7 @@ pub fn rmdir(dir_path: []const u8) DeleteDirError!void { @compileError("rmdir is not supported in WASI; use unlinkat instead"); } else if (builtin.os.tag == .windows) { const dir_path_w = try windows.sliceToPrefixedFileW(dir_path); - return windows.RemoveDirectoryW(dir_path_w.span().ptr); + return rmdirW(dir_path_w.span()); } else { const dir_path_c = try toPosixPath(dir_path); return rmdirZ(&dir_path_c); @@ -2244,7 +2283,7 @@ pub const rmdirC = @compileError("deprecated: renamed to rmdirZ"); pub fn rmdirZ(dir_path: [*:0]const u8) DeleteDirError!void { if (builtin.os.tag == .windows) { const dir_path_w = try windows.cStrToPrefixedFileW(dir_path); - return windows.RemoveDirectoryW(dir_path_w.span().ptr); + return rmdirW(dir_path_w.span()); } switch (errno(system.rmdir(dir_path))) { 0 => return, @@ -2265,6 +2304,14 @@ pub fn rmdirZ(dir_path: [*:0]const u8) DeleteDirError!void { } } +/// Windows-only. Same as `rmdir` except the parameter is WTF16 encoded. +pub fn rmdirW(dir_path_w: []const u16) DeleteDirError!void { + return windows.DeleteFile(dir_path_w, .{ .dir = std.fs.cwd().fd, .remove_dir = true }) catch |err| switch (err) { + error.IsDir => unreachable, + else => |e| return e, + }; +} + pub const ChangeCurDirError = error{ AccessDenied, FileSystem, @@ -2354,7 +2401,8 @@ pub fn readlink(file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 { if (builtin.os.tag == .wasi) { @compileError("readlink is not supported in WASI; use readlinkat instead"); } else if (builtin.os.tag == .windows) { - return windows.ReadLink(std.fs.cwd().fd, file_path, out_buffer); + const file_path_w = try windows.sliceToPrefixedFileW(file_path); + return readlinkW(file_path_w.span(), out_buffer); } else { const file_path_c = try toPosixPath(file_path); return readlinkZ(&file_path_c, out_buffer); @@ -2363,17 +2411,17 @@ pub fn readlink(file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 { pub const readlinkC = @compileError("deprecated: renamed to readlinkZ"); -/// Windows-only. Same as `readlink` except `file_path` is null-terminated, WTF16 encoded. +/// Windows-only. Same as `readlink` except `file_path` is WTF16 encoded. /// See also `readlinkZ`. -pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { - return windows.ReadLinkW(std.fs.cwd().fd, file_path, out_buffer); +pub fn readlinkW(file_path: []const u16, out_buffer: []u8) ReadLinkError![]u8 { + return windows.ReadLink(std.fs.cwd().fd, file_path, out_buffer); } /// Same as `readlink` except `file_path` is null-terminated. pub fn readlinkZ(file_path: [*:0]const u8, out_buffer: []u8) ReadLinkError![]u8 { if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToWin32PrefixedFileW(file_path); - return readlinkW(file_path_w.span().ptr, out_buffer); + return readlinkW(file_path_w.span(), out_buffer); } const rc = system.readlink(file_path, out_buffer.ptr, out_buffer.len); switch (errno(rc)) { @@ -2399,7 +2447,8 @@ pub fn readlinkat(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) ReadLink return readlinkatWasi(dirfd, file_path, out_buffer); } if (builtin.os.tag == .windows) { - return windows.ReadLink(dirfd, file_path, out_buffer); + const file_path_w = try windows.sliceToPrefixedFileW(file_path); + return readlinkatW(dirfd, file_path_w.span(), out_buffer); } const file_path_c = try toPosixPath(file_path); return readlinkatZ(dirfd, &file_path_c, out_buffer); @@ -2429,8 +2478,8 @@ pub fn readlinkatWasi(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) Read /// Windows-only. Same as `readlinkat` except `file_path` is null-terminated, WTF16 encoded. /// See also `readlinkat`. -pub fn readlinkatW(dirfd: fd_t, file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { - return windows.ReadLinkW(dirfd, file_path, out_buffer); +pub fn readlinkatW(dirfd: fd_t, file_path: []const u16, out_buffer: []u8) ReadLinkError![]u8 { + return windows.ReadLink(dirfd, file_path, out_buffer); } /// Same as `readlinkat` except `file_path` is null-terminated. @@ -2438,7 +2487,7 @@ pub fn readlinkatW(dirfd: fd_t, file_path: [*:0]const u16, out_buffer: []u8) Rea pub fn readlinkatZ(dirfd: fd_t, file_path: [*:0]const u8, out_buffer: []u8) ReadLinkError![]u8 { if (builtin.os.tag == .windows) { const file_path_w = try windows.cStrToPrefixedFileW(file_path); - return readlinkatW(dirfd, file_path_w.span().ptr, out_buffer); + return readlinkatW(dirfd, file_path_w.span(), out_buffer); } const rc = system.readlinkat(dirfd, file_path, out_buffer.ptr, out_buffer.len); switch (errno(rc)) { @@ -3959,7 +4008,7 @@ pub const RealPathError = error{ pub fn realpath(pathname: []const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { if (builtin.os.tag == .windows) { const pathname_w = try windows.sliceToPrefixedFileW(pathname); - return realpathW(pathname_w.span().ptr, out_buffer); + return realpathW(pathname_w.span(), out_buffer); } if (builtin.os.tag == .wasi) { @compileError("Use std.fs.wasi.PreopenList to obtain valid Dir handles instead of using absolute paths"); @@ -3974,7 +4023,7 @@ pub const realpathC = @compileError("deprecated: renamed realpathZ"); pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { if (builtin.os.tag == .windows) { const pathname_w = try windows.cStrToPrefixedFileW(pathname); - return realpathW(pathname_w.span().ptr, out_buffer); + return realpathW(pathname_w.span(), out_buffer); } if (builtin.os.tag == .linux and !builtin.link_libc) { const fd = openZ(pathname, linux.O_PATH | linux.O_NONBLOCK | linux.O_CLOEXEC, 0) catch |err| switch (err) { @@ -4010,22 +4059,43 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP return mem.spanZ(result_path); } -/// Same as `realpath` except `pathname` is null-terminated and UTF16LE-encoded. -/// TODO use ntdll for better semantics -pub fn realpathW(pathname: [*:0]const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { - const h_file = try windows.CreateFileW( - pathname, - windows.GENERIC_READ, - windows.FILE_SHARE_READ, - null, - windows.OPEN_EXISTING, - windows.FILE_FLAG_BACKUP_SEMANTICS, - null, - ); - defer windows.CloseHandle(h_file); +/// Same as `realpath` except `pathname` is UTF16LE-encoded. +/// TODO use ntdll to emulate `GetFinalPathNameByHandleW` routine +pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { + const w = windows; - var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined; - const wide_slice = try windows.GetFinalPathNameByHandleW(h_file, &wide_buf, wide_buf.len, windows.VOLUME_NAME_DOS); + const dir = std.fs.cwd().fd; + const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; + const share_access = w.FILE_SHARE_READ; + const creation = w.FILE_OPEN; + const h_file = blk: { + const res = w.OpenFile(pathname, .{ + .dir = dir, + .access_mask = access_mask, + .share_access = share_access, + .creation = creation, + .io_mode = .blocking, + }) catch |err| switch (err) { + error.IsDir => break :blk w.OpenFile(pathname, .{ + .dir = dir, + .access_mask = access_mask, + .share_access = share_access, + .creation = creation, + .io_mode = .blocking, + .open_dir = true, + }) catch |er| switch (er) { + error.WouldBlock => unreachable, + else => |e2| return e2, + }, + error.WouldBlock => unreachable, + else => |e| return e, + }; + break :blk res; + }; + defer w.CloseHandle(h_file); + + var wide_buf: [w.PATH_MAX_WIDE]u16 = undefined; + const wide_slice = try w.GetFinalPathNameByHandleW(h_file, &wide_buf, wide_buf.len, w.VOLUME_NAME_DOS); // Windows returns \\?\ prepended to the path. // We strip it to make this function consistent across platforms. diff --git a/lib/std/os/bits/windows.zig b/lib/std/os/bits/windows.zig index 4988cafbe0..53a590ff1e 100644 --- a/lib/std/os/bits/windows.zig +++ b/lib/std/os/bits/windows.zig @@ -237,3 +237,28 @@ pub const IPPROTO_TCP = ws2_32.IPPROTO_TCP; pub const IPPROTO_UDP = ws2_32.IPPROTO_UDP; pub const IPPROTO_ICMPV6 = ws2_32.IPPROTO_ICMPV6; pub const IPPROTO_RM = ws2_32.IPPROTO_RM; + +pub const O_RDONLY = 0o0; +pub const O_WRONLY = 0o1; +pub const O_RDWR = 0o2; + +pub const O_CREAT = 0o100; +pub const O_EXCL = 0o200; +pub const O_NOCTTY = 0o400; +pub const O_TRUNC = 0o1000; +pub const O_APPEND = 0o2000; +pub const O_NONBLOCK = 0o4000; +pub const O_DSYNC = 0o10000; +pub const O_SYNC = 0o4010000; +pub const O_RSYNC = 0o4010000; +pub const O_DIRECTORY = 0o200000; +pub const O_NOFOLLOW = 0o400000; +pub const O_CLOEXEC = 0o2000000; + +pub const O_ASYNC = 0o20000; +pub const O_DIRECT = 0o40000; +pub const O_LARGEFILE = 0; +pub const O_NOATIME = 0o1000000; +pub const O_PATH = 0o10000000; +pub const O_TMPFILE = 0o20200000; +pub const O_NDELAY = O_NONBLOCK; \ No newline at end of file diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 7310562d64..b39e239ed4 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -3,6 +3,7 @@ const os = std.os; const testing = std.testing; const expect = testing.expect; const expectEqual = testing.expectEqual; +const expectError = testing.expectError; const io = std.io; const fs = std.fs; const mem = std.mem; @@ -19,6 +20,95 @@ const tmpDir = std.testing.tmpDir; const Dir = std.fs.Dir; const ArenaAllocator = std.heap.ArenaAllocator; +test "open smoke test" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + // TODO verify file attributes using `fstat` + + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + // Get base abs path + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + + const base_path = blk: { + const relative_path = try fs.path.join(&arena.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp.sub_path[0..] }); + break :blk try fs.realpathAlloc(&arena.allocator, relative_path); + }; + + var file_path: []u8 = undefined; + var fd: os.fd_t = undefined; + const mode: os.mode_t = if (builtin.os.tag == .windows) 0 else 0o666; + + // Create some file using `open`. + file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); + fd = try os.open(file_path, os.O_RDWR | os.O_CREAT | os.O_EXCL, mode); + os.close(fd); + + // Try this again with the same flags. This op should fail with error.PathAlreadyExists. + file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); + expectError(error.PathAlreadyExists, os.open(file_path, os.O_RDWR | os.O_CREAT | os.O_EXCL, mode)); + + // Try opening without `O_EXCL` flag. + file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); + fd = try os.open(file_path, os.O_RDWR | os.O_CREAT, mode); + os.close(fd); + + // Try opening as a directory which should fail. + file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); + expectError(error.NotDir, os.open(file_path, os.O_RDWR | os.O_DIRECTORY, mode)); + + // Create some directory + file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_dir" }); + try os.mkdir(file_path, mode); + + // Open dir using `open` + file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_dir" }); + fd = try os.open(file_path, os.O_RDONLY | os.O_DIRECTORY, mode); + os.close(fd); + + // Try opening as file which should fail. + file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_dir" }); + expectError(error.IsDir, os.open(file_path, os.O_RDWR, mode)); +} + +test "openat smoke test" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + // TODO verify file attributes using `fstatat` + + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + var fd: os.fd_t = undefined; + const mode: os.mode_t = if (builtin.os.tag == .windows) 0 else 0o666; + + // Create some file using `openat`. + fd = try os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_CREAT | os.O_EXCL, mode); + os.close(fd); + + // Try this again with the same flags. This op should fail with error.PathAlreadyExists. + expectError(error.PathAlreadyExists, os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_CREAT | os.O_EXCL, mode)); + + // Try opening without `O_EXCL` flag. + fd = try os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_CREAT, mode); + os.close(fd); + + // Try opening as a directory which should fail. + expectError(error.NotDir, os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_DIRECTORY, mode)); + + // Create some directory + try os.mkdirat(tmp.dir.fd, "some_dir", mode); + + // Open dir using `open` + fd = try os.openat(tmp.dir.fd, "some_dir", os.O_RDONLY | os.O_DIRECTORY, mode); + os.close(fd); + + // Try opening as file which should fail. + expectError(error.IsDir, os.openat(tmp.dir.fd, "some_dir", os.O_RDWR, mode)); +} + test "symlink with relative paths" { if (builtin.os.tag == .wasi) return error.SkipZigTest; @@ -27,7 +117,7 @@ test "symlink with relative paths" { try cwd.writeFile("file.txt", "nonsense"); if (builtin.os.tag == .windows) { - try os.windows.CreateSymbolicLink(cwd.fd, "symlinked", "file.txt", false); + try os.windows.CreateSymbolicLink(cwd.fd, &[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' }, &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, false); } else { try os.symlink("file.txt", "symlinked"); } @@ -85,7 +175,7 @@ test "readlinkat" { // create a symbolic link if (builtin.os.tag == .windows) { - try os.windows.CreateSymbolicLink(tmp.dir.fd, "link", "file.txt", false); + try os.windows.CreateSymbolicLink(tmp.dir.fd, &[_]u16{ 'l', 'i', 'n', 'k' }, &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, false); } else { try os.symlinkat("file.txt", tmp.dir.fd, "link"); } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index b0b70139fd..bf41d0ae93 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -25,76 +25,11 @@ pub usingnamespace @import("windows/bits.zig"); pub const self_process_handle = @intToPtr(HANDLE, maxInt(usize)); -pub const CreateFileError = error{ - SharingViolation, - PathAlreadyExists, - - /// When any of the path components can not be found or the file component can not - /// be found. Some operating systems distinguish between path components not found and - /// file components not found, but they are collapsed into FileNotFound to gain - /// consistency across operating systems. - FileNotFound, - - AccessDenied, - PipeBusy, - NameTooLong, - - /// On Windows, file paths must be valid Unicode. - InvalidUtf8, - - /// On Windows, file paths cannot contain these characters: - /// '/', '*', '?', '"', '<', '>', '|' - BadPathName, - - Unexpected, -}; - -pub fn CreateFile( - file_path: []const u8, - desired_access: DWORD, - share_mode: DWORD, - lpSecurityAttributes: ?LPSECURITY_ATTRIBUTES, - creation_disposition: DWORD, - flags_and_attrs: DWORD, - hTemplateFile: ?HANDLE, -) CreateFileError!HANDLE { - const file_path_w = try sliceToPrefixedFileW(file_path); - return CreateFileW(file_path_w.span().ptr, desired_access, share_mode, lpSecurityAttributes, creation_disposition, flags_and_attrs, hTemplateFile); -} - -pub fn CreateFileW( - file_path_w: [*:0]const u16, - desired_access: DWORD, - share_mode: DWORD, - lpSecurityAttributes: ?LPSECURITY_ATTRIBUTES, - creation_disposition: DWORD, - flags_and_attrs: DWORD, - hTemplateFile: ?HANDLE, -) CreateFileError!HANDLE { - const result = kernel32.CreateFileW(file_path_w, desired_access, share_mode, lpSecurityAttributes, creation_disposition, flags_and_attrs, hTemplateFile); - - if (result == INVALID_HANDLE_VALUE) { - switch (kernel32.GetLastError()) { - .SHARING_VIOLATION => return error.SharingViolation, - .ALREADY_EXISTS => return error.PathAlreadyExists, - .FILE_EXISTS => return error.PathAlreadyExists, - .FILE_NOT_FOUND => return error.FileNotFound, - .PATH_NOT_FOUND => return error.FileNotFound, - .ACCESS_DENIED => return error.AccessDenied, - .PIPE_BUSY => return error.PipeBusy, - .FILENAME_EXCED_RANGE => return error.NameTooLong, - else => |err| return unexpectedError(err), - } - } - - return result; -} - pub const OpenError = error{ IsDir, + NotDir, FileNotFound, NoDevice, - SharingViolation, AccessDenied, PipeBusy, PathAlreadyExists, @@ -111,15 +46,21 @@ pub const OpenFileOptions = struct { share_access_nonblocking: bool = false, creation: ULONG, io_mode: std.io.ModeOverride, + /// If true, tries to open path as a directory. + /// Defaults to false. + open_dir: bool = false, + /// If false, tries to open path as a reparse point without dereferencing it. + /// Defaults to true. + follow_symlinks: bool = true, }; /// TODO when share_access_nonblocking is false, this implementation uses /// untinterruptible sleep() to block. This is not the final iteration of the API. pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HANDLE { - if (mem.eql(u16, sub_path_w, &[_]u16{'.'})) { + if (mem.eql(u16, sub_path_w, &[_]u16{'.'}) and !options.open_dir) { return error.IsDir; } - if (mem.eql(u16, sub_path_w, &[_]u16{ '.', '.' })) { + if (mem.eql(u16, sub_path_w, &[_]u16{ '.', '.' }) and !options.open_dir) { return error.IsDir; } @@ -142,11 +83,13 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN .SecurityQualityOfService = null, }; var io: IO_STATUS_BLOCK = undefined; + const blocking_flag: ULONG = if (options.io_mode == .blocking) FILE_SYNCHRONOUS_IO_NONALERT else 0; + const file_or_dir_flag: ULONG = if (options.open_dir) FILE_DIRECTORY_FILE else FILE_NON_DIRECTORY_FILE; + // If we're not following symlinks, we need to ensure we don't pass in any synchronization flags such as FILE_SYNCHRONOUS_IO_NONALERT. + const flags: ULONG = if (options.follow_symlinks) file_or_dir_flag | blocking_flag else file_or_dir_flag | FILE_OPEN_REPARSE_POINT; var delay: usize = 1; while (true) { - var flags: ULONG = undefined; - const blocking_flag: ULONG = if (options.io_mode == .blocking) FILE_SYNCHRONOUS_IO_NONALERT else 0; const rc = ntdll.NtCreateFile( &result, options.access_mask, @@ -156,7 +99,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN FILE_ATTRIBUTE_NORMAL, options.share_access, options.creation, - FILE_NON_DIRECTORY_FILE | blocking_flag, + flags, null, 0, ); @@ -184,6 +127,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN .OBJECT_PATH_SYNTAX_BAD => unreachable, .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, .FILE_IS_A_DIRECTORY => return error.IsDir, + .NOT_A_DIRECTORY => return error.NotDir, else => return unexpectedStatus(rc), } } @@ -638,27 +582,14 @@ pub const CreateSymbolicLinkError = error{ PathAlreadyExists, FileNotFound, NameTooLong, - InvalidUtf8, - BadPathName, NoDevice, Unexpected, }; pub fn CreateSymbolicLink( dir: ?HANDLE, - sym_link_path: []const u8, - target_path: []const u8, - is_directory: bool, -) CreateSymbolicLinkError!void { - const sym_link_path_w = try sliceToPrefixedFileW(sym_link_path); - const target_path_w = try sliceToPrefixedFileW(target_path); - return CreateSymbolicLinkW(dir, sym_link_path_w.span(), target_path_w.span(), is_directory); -} - -pub fn CreateSymbolicLinkW( - dir: ?HANDLE, - sym_link_path: [:0]const u16, - target_path: [:0]const u16, + sym_link_path: []const u16, + target_path: []const u16, is_directory: bool, ) CreateSymbolicLinkError!void { const SYMLINK_DATA = extern struct { @@ -672,71 +603,19 @@ pub fn CreateSymbolicLinkW( Flags: ULONG, }; - var symlink_handle: HANDLE = undefined; - if (is_directory) { - const sym_link_len_bytes = math.cast(u16, sym_link_path.len * 2) catch |err| switch (err) { - error.Overflow => return error.NameTooLong, - }; - var nt_name = UNICODE_STRING{ - .Length = sym_link_len_bytes, - .MaximumLength = sym_link_len_bytes, - .Buffer = @intToPtr([*]u16, @ptrToInt(sym_link_path.ptr)), - }; - - if (sym_link_path[0] == '.' and sym_link_path[1] == 0) { - // Windows does not recognize this, but it does work with empty string. - nt_name.Length = 0; - } - - var attr = OBJECT_ATTRIBUTES{ - .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sym_link_path)) null else dir, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - - var io: IO_STATUS_BLOCK = undefined; - const rc = ntdll.NtCreateFile( - &symlink_handle, - GENERIC_READ | SYNCHRONIZE | FILE_WRITE_ATTRIBUTES, - &attr, - &io, - null, - FILE_ATTRIBUTE_NORMAL, - FILE_SHARE_READ, - FILE_CREATE, - FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_FOR_BACKUP_INTENT, - null, - 0, - ); - switch (rc) { - .SUCCESS => {}, - .OBJECT_NAME_INVALID => unreachable, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NO_MEDIA_IN_DEVICE => return error.NoDevice, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - .OBJECT_PATH_SYNTAX_BAD => unreachable, - .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - else => return unexpectedStatus(rc), - } - } else { - symlink_handle = OpenFile(sym_link_path, .{ - .access_mask = SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE, - .dir = dir, - .creation = FILE_CREATE, - .io_mode = .blocking, - }) catch |err| switch (err) { - error.WouldBlock => unreachable, - error.IsDir => return error.PathAlreadyExists, - error.PipeBusy => unreachable, - error.SharingViolation => return error.AccessDenied, - else => |e| return e, - }; - } + const symlink_handle = OpenFile(sym_link_path, .{ + .access_mask = SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE, + .dir = dir, + .creation = FILE_CREATE, + .io_mode = .blocking, + .open_dir = is_directory, + }) catch |err| switch (err) { + error.IsDir => return error.PathAlreadyExists, + error.NotDir => unreachable, + error.WouldBlock => unreachable, + error.PipeBusy => unreachable, + else => |e| return e, + }; defer CloseHandle(symlink_handle); // prepare reparse data buffer @@ -767,44 +646,32 @@ pub const ReadLinkError = error{ Unexpected, NameTooLong, UnsupportedReparsePointType, - InvalidUtf8, - BadPathName, }; -pub fn ReadLink( - dir: ?HANDLE, - sub_path: []const u8, - out_buffer: []u8, -) ReadLinkError![]u8 { - const sub_path_w = try sliceToPrefixedFileW(sub_path); - return ReadLinkW(dir, sub_path_w.span().ptr, out_buffer); -} - -pub fn ReadLinkW(dir: ?HANDLE, sub_path_w: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { - const path_len_bytes = math.cast(u16, mem.lenZ(sub_path_w) * 2) catch |err| switch (err) { +pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLinkError![]u8 { + // Here, we use `NtCreateFile` to shave off one syscall if we were to use `OpenFile` wrapper. + // With the latter, we'd need to call `NtCreateFile` twice, once for file symlink, and if that + // failed, again for dir symlink. Omitting any mention of file/dir flags makes it possible + // to open the symlink there and then. + const path_len_bytes = math.cast(u16, sub_path_w.len * 2) catch |err| switch (err) { error.Overflow => return error.NameTooLong, }; var nt_name = UNICODE_STRING{ .Length = path_len_bytes, .MaximumLength = path_len_bytes, - .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w)), + .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w.ptr)), }; - - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - // Windows does not recognize this, but it does work with empty string. - nt_name.Length = 0; - } - var attr = OBJECT_ATTRIBUTES{ .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w)) null else dir, + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else dir, .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. .ObjectName = &nt_name, .SecurityDescriptor = null, .SecurityQualityOfService = null, }; - var io: IO_STATUS_BLOCK = undefined; var result_handle: HANDLE = undefined; + var io: IO_STATUS_BLOCK = undefined; + const rc = ntdll.NtCreateFile( &result_handle, FILE_READ_ATTRIBUTES, @@ -878,24 +745,69 @@ pub const DeleteFileError = error{ NameTooLong, FileBusy, Unexpected, + NotDir, + IsDir, }; -pub fn DeleteFile(filename: []const u8) DeleteFileError!void { - const filename_w = try sliceToPrefixedFileW(filename); - return DeleteFileW(filename_w.span().ptr); -} +pub const DeleteFileOptions = struct { + dir: ?HANDLE, + remove_dir: bool = false, +}; -pub fn DeleteFileW(filename: [*:0]const u16) DeleteFileError!void { - if (kernel32.DeleteFileW(filename) == 0) { - switch (kernel32.GetLastError()) { - .FILE_NOT_FOUND => return error.FileNotFound, - .PATH_NOT_FOUND => return error.FileNotFound, - .ACCESS_DENIED => return error.AccessDenied, - .FILENAME_EXCED_RANGE => return error.NameTooLong, - .INVALID_PARAMETER => return error.NameTooLong, - .SHARING_VIOLATION => return error.FileBusy, - else => |err| return unexpectedError(err), - } +pub fn DeleteFile(sub_path_w: []const u16, options: DeleteFileOptions) DeleteFileError!void { + const create_options_flags: ULONG = if (options.remove_dir) + FILE_DELETE_ON_CLOSE | FILE_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT + else + FILE_DELETE_ON_CLOSE | FILE_NON_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT; // would we ever want to delete the target instead? + + const path_len_bytes = @intCast(u16, sub_path_w.len * 2); + var nt_name = UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + // The Windows API makes this mutable, but it will not mutate here. + .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w.ptr)), + }; + + if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { + // Windows does not recognize this, but it does work with empty string. + nt_name.Length = 0; + } + if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { + // Can't remove the parent directory with an open handle. + return error.FileBusy; + } + + var attr = OBJECT_ATTRIBUTES{ + .Length = @sizeOf(OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else options.dir, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + var io: IO_STATUS_BLOCK = undefined; + var tmp_handle: HANDLE = undefined; + var rc = ntdll.NtCreateFile( + &tmp_handle, + SYNCHRONIZE | DELETE, + &attr, + &io, + null, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + create_options_flags, + null, + 0, + ); + switch (rc) { + .SUCCESS => return CloseHandle(tmp_handle), + .OBJECT_NAME_INVALID => unreachable, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .INVALID_PARAMETER => unreachable, + .FILE_IS_A_DIRECTORY => return error.IsDir, + .NOT_A_DIRECTORY => return error.NotDir, + else => return unexpectedStatus(rc), } } @@ -915,103 +827,6 @@ pub fn MoveFileExW(old_path: [*:0]const u16, new_path: [*:0]const u16, flags: DW } } -pub const CreateDirectoryError = error{ - NameTooLong, - PathAlreadyExists, - FileNotFound, - NoDevice, - AccessDenied, - InvalidUtf8, - BadPathName, - Unexpected, -}; - -/// Returns an open directory handle which the caller is responsible for closing with `CloseHandle`. -pub fn CreateDirectory(dir: ?HANDLE, pathname: []const u8, sa: ?*SECURITY_ATTRIBUTES) CreateDirectoryError!HANDLE { - const pathname_w = try sliceToPrefixedFileW(pathname); - return CreateDirectoryW(dir, pathname_w.span().ptr, sa); -} - -/// Same as `CreateDirectory` except takes a WTF-16 encoded path. -pub fn CreateDirectoryW( - dir: ?HANDLE, - sub_path_w: [*:0]const u16, - sa: ?*SECURITY_ATTRIBUTES, -) CreateDirectoryError!HANDLE { - const path_len_bytes = math.cast(u16, mem.lenZ(sub_path_w) * 2) catch |err| switch (err) { - error.Overflow => return error.NameTooLong, - }; - var nt_name = UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w)), - }; - - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - // Windows does not recognize this, but it does work with empty string. - nt_name.Length = 0; - } - - var attr = OBJECT_ATTRIBUTES{ - .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w)) null else dir, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = if (sa) |ptr| ptr.lpSecurityDescriptor else null, - .SecurityQualityOfService = null, - }; - var io: IO_STATUS_BLOCK = undefined; - var result_handle: HANDLE = undefined; - const rc = ntdll.NtCreateFile( - &result_handle, - GENERIC_READ | SYNCHRONIZE, - &attr, - &io, - null, - FILE_ATTRIBUTE_NORMAL, - FILE_SHARE_READ, - FILE_CREATE, - FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, - null, - 0, - ); - switch (rc) { - .SUCCESS => return result_handle, - .OBJECT_NAME_INVALID => unreachable, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NO_MEDIA_IN_DEVICE => return error.NoDevice, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - .OBJECT_PATH_SYNTAX_BAD => unreachable, - .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - else => return unexpectedStatus(rc), - } -} - -pub const RemoveDirectoryError = error{ - FileNotFound, - DirNotEmpty, - Unexpected, - NotDir, -}; - -pub fn RemoveDirectory(dir_path: []const u8) RemoveDirectoryError!void { - const dir_path_w = try sliceToPrefixedFileW(dir_path); - return RemoveDirectoryW(dir_path_w.span().ptr); -} - -pub fn RemoveDirectoryW(dir_path_w: [*:0]const u16) RemoveDirectoryError!void { - if (kernel32.RemoveDirectoryW(dir_path_w) == 0) { - switch (kernel32.GetLastError()) { - .PATH_NOT_FOUND => return error.FileNotFound, - .DIR_NOT_EMPTY => return error.DirNotEmpty, - .DIRECTORY => return error.NotDir, - else => |err| return unexpectedError(err), - } - } -} - pub const GetStdHandleError = error{ NoStandardHandleAttached, Unexpected, @@ -1493,8 +1308,7 @@ pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace { } /// Converts the path `s` to WTF16, null-terminated. If the path is absolute, -/// it will get NT-style prefix `\??\` prepended automatically. For prepending -/// Win32-style prefix, see `sliceToWin32PrefixedFileW` instead. +/// it will get NT-style prefix `\??\` prepended automatically. pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace { // TODO https://github.com/ziglang/zig/issues/2765 var path_space: PathSpace = undefined;