From cc83d92b0b12f5b3fa6462ed878cd7bec7412940 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Sun, 12 Jul 2020 23:03:09 +0200 Subject: [PATCH 01/28] Start drafting out os.readlink on Windows --- lib/std/os.zig | 32 ++++++++++++++++++++++++++++++-- lib/std/os/windows/bits.zig | 27 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index dfb47208ca9c..983f7f9a90cd 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -2355,6 +2355,8 @@ pub const ReadLinkError = error{ FileNotFound, SystemResources, NotDir, + /// Windows-only. + UnsupportedSymlinkType, } || UnexpectedError; /// Read value of a symbolic link. @@ -2373,9 +2375,35 @@ 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` expecte `file_path` is null-terminated, WTF16 encoded. -/// Seel also `readlinkZ`. +/// Windows-only. Same as `readlink` except `file_path` is null-terminated, WTF16 encoded. +/// See also `readlinkZ`. pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { + const handle = windows.OpenFile(file_path, .{ + .access_mask = 0, + .creation = c.FILE_OPEN_REPARSE_POINT | c.FILE_LIST_DIRECTORY, + .io_mode = 0, + }) catch |err| { + switch (err) { + error.IsDir => unreachable, + error.NoDevice => return error.FileNotFound, + error.SharingViolation => return error.AccessDenied, + error.PipeBusy => unreachable, + error.PathAlreadyExists => unreachable, + error.WouldBlock => unreachable, + else => return err, + } + }; + var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; + _ = try windows.DeviceIoControl(handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); + const reparse_struct = @bitCast(windows._REPARSE_DATA_BUFFER, reparse_buf[0..@sizeOf(windows._REPARSE_DATA_BUFFER)]); + switch (reparse_struct.ReparseTag) { + windows.IO_REPARSE_TAG_SYMLINK => {}, + windows.IO_REPARSE_TAG_MOUNT_POINT => {}, + else => |value| { + std.debug.print("unsupported symlink type: {}", value); + return error.UnsupportedSymlinkType; + }, + } @compileError("TODO implement readlink for Windows"); } diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index a6a9e50d53d4..6182dc5de826 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -1542,3 +1542,30 @@ pub const POSVERSIONINFOW = *OSVERSIONINFOW; pub const LPOSVERSIONINFOW = *OSVERSIONINFOW; pub const RTL_OSVERSIONINFOW = OSVERSIONINFOW; pub const PRTL_OSVERSIONINFOW = *RTL_OSVERSIONINFOW; + +pub const _REPARSE_DATA_BUFFER = extern struct { + ReparseTag: ULONG, ReparseDataLength: USHORT, Reserved: USHORT, u: extern union { + SymbolicLinkReparseBuffer: extern struct { + SubstituteNameOffset: USHORT, + SubstituteNameLength: USHORT, + PrintNameOffset: USHORT, + PrintNameLength: USHORT, + Flags: ULONG, + PathBuffer: [1]WCHAR, + }, + MountPointReparseBuffer: extern struct { + SubstituteNameOffset: USHORT, + SubstituteNameLength: USHORT, + PrintNameOffset: USHORT, + PrintNameLength: USHORT, + PathBuffer: [1]WCHAR, + }, + GenericReparseBuffer: extern struct { + DataBuffer: [1]UCHAR, + }, + } +}; +pub const MAXIMUM_REPARSE_DATA_BUFFER_SIZE: usize = 16 * 1024; +pub const FSCTL_GET_REPARSE_POINT: DWORD = 0x900a8; +pub const IO_REPARSE_TAG_SYMLINK: ULONG = 0xa000000c; +pub const IO_REPARSE_TAG_MOUNT_POINT: ULONG = 0xa0000003; From 515c663cd6e31d5f8ae07e804f2b32b99d025895 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 13 Jul 2020 08:01:01 +0200 Subject: [PATCH 02/28] Add readlink smoke test --- lib/std/os.zig | 28 +++++++++++++++++----------- lib/std/os/test.zig | 31 +++++++++++++++++++++++++++++++ lib/std/os/windows/bits.zig | 2 +- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 983f7f9a90cd..ab54ba5fe88b 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -2355,6 +2355,8 @@ pub const ReadLinkError = error{ FileNotFound, SystemResources, NotDir, + InvalidUtf8, + BadPathName, /// Windows-only. UnsupportedSymlinkType, } || UnexpectedError; @@ -2366,7 +2368,7 @@ pub fn readlink(file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 { @compileError("readlink is not supported in WASI; use readlinkat instead"); } else if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); - return readlinkW(file_path_w.span().ptr, out_buffer); + return readlinkW(file_path_w.span(), out_buffer); } else { const file_path_c = try toPosixPath(file_path); return readlinkZ(&file_path_c, out_buffer); @@ -2377,11 +2379,11 @@ pub const readlinkC = @compileError("deprecated: renamed to readlinkZ"); /// Windows-only. Same as `readlink` except `file_path` is null-terminated, WTF16 encoded. /// See also `readlinkZ`. -pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { +pub fn readlinkW(file_path: []const u16, out_buffer: []u8) ReadLinkError![]u8 { const handle = windows.OpenFile(file_path, .{ .access_mask = 0, - .creation = c.FILE_OPEN_REPARSE_POINT | c.FILE_LIST_DIRECTORY, - .io_mode = 0, + .creation = windows.FILE_OPEN_REPARSE_POINT | windows.FILE_LIST_DIRECTORY, + .io_mode = std.io.default_mode, }) catch |err| { switch (err) { error.IsDir => unreachable, @@ -2390,28 +2392,32 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 error.PipeBusy => unreachable, error.PathAlreadyExists => unreachable, error.WouldBlock => unreachable, - else => return err, + else => |e| return e, } }; var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; _ = try windows.DeviceIoControl(handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); - const reparse_struct = @bitCast(windows._REPARSE_DATA_BUFFER, reparse_buf[0..@sizeOf(windows._REPARSE_DATA_BUFFER)]); + const reparse_struct = mem.bytesAsValue(windows._REPARSE_DATA_BUFFER, reparse_buf[0..@sizeOf(windows._REPARSE_DATA_BUFFER)]); switch (reparse_struct.ReparseTag) { - windows.IO_REPARSE_TAG_SYMLINK => {}, - windows.IO_REPARSE_TAG_MOUNT_POINT => {}, + windows.IO_REPARSE_TAG_SYMLINK => { + std.debug.warn("got symlink!", .{}); + }, + windows.IO_REPARSE_TAG_MOUNT_POINT => { + std.debug.warn("got mount point!", .{}); + }, else => |value| { - std.debug.print("unsupported symlink type: {}", value); + std.debug.warn("unsupported symlink type: {}", .{value}); return error.UnsupportedSymlinkType; }, } - @compileError("TODO implement readlink for Windows"); + @panic("Oh no!"); } /// 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.cStrToPrefixedFileW(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)) { diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index e508f5ae20f3..b3f0136cf6af 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -17,6 +17,7 @@ const AtomicRmwOp = builtin.AtomicRmwOp; const AtomicOrder = builtin.AtomicOrder; const tmpDir = std.testing.tmpDir; const Dir = std.fs.Dir; +const ArenaAllocator = std.heap.ArenaAllocator; test "fstatat" { // enable when `fstat` and `fstatat` are implemented on Windows @@ -40,6 +41,36 @@ test "fstatat" { expectEqual(stat, statat); } +test "readlink" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + // create file + try tmp.dir.writeFile("file.txt", "nonsense"); + + // get paths + // TODO: use Dir's realpath function once that exists + 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); + }; + const target_path = try fs.path.join(&arena.allocator, &[_][]const u8{"file.txt"}); + const symlink_path = try fs.path.join(&arena.allocator, &[_][]const u8{"symlinked"}); + + // create symbolic link by path + try os.symlink(target_path, symlink_path); + + // now, read the link and verify + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const given = try os.readlink(symlink_path, buffer[0..]); + expect(mem.eql(u8, symlink_path, given)); +} + test "readlinkat" { // enable when `readlinkat` and `symlinkat` are implemented on Windows if (builtin.os.tag == .windows) return error.SkipZigTest; diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index 6182dc5de826..998d4a809135 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -488,7 +488,7 @@ pub const FILE_OPEN_BY_FILE_ID = 0x00002000; pub const FILE_OPEN_FOR_BACKUP_INTENT = 0x00004000; pub const FILE_NO_COMPRESSION = 0x00008000; pub const FILE_RESERVE_OPFILTER = 0x00100000; -pub const FILE_TRANSACTED_MODE = 0x00200000; +pub const FILE_OPEN_REPARSE_POINT = 0x00200000; pub const FILE_OPEN_OFFLINE_FILE = 0x00400000; pub const FILE_OPEN_FOR_FREE_SPACE_QUERY = 0x00800000; From 791795a63a34bc69af59717d5eff5d4c76349756 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 13 Jul 2020 08:29:11 +0200 Subject: [PATCH 03/28] Finish symlink implementation on Windows --- lib/std/os.zig | 15 ++++++++++++++- lib/std/os/windows.zig | 35 +++++++++++++++++++++++++++++++---- lib/std/os/windows/bits.zig | 4 ++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index ab54ba5fe88b..67b7dfe05b05 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1541,6 +1541,10 @@ pub const SymLinkError = error{ /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. +/// On Windows, it is only legal to create a symbolic link to an existing resource. Furthermore, +/// 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 instead, see `symlinkW` for more information how to +/// do that. /// If `sym_link_path` exists, it will not be overwritten. /// See also `symlinkC` and `symlinkW`. pub fn symlink(target_path: []const u8, sym_link_path: []const u8) SymLinkError!void { @@ -1550,7 +1554,7 @@ pub fn symlink(target_path: []const u8, sym_link_path: []const u8) SymLinkError! if (builtin.os.tag == .windows) { const target_path_w = try windows.sliceToPrefixedFileW(target_path); const sym_link_path_w = try windows.sliceToPrefixedFileW(sym_link_path); - return windows.CreateSymbolicLinkW(sym_link_path_w.span().ptr, target_path_w.span().ptr, 0); + return symlinkW(sym_link_path_w.span().ptr, target_path_w.span().ptr); } const target_path_c = try toPosixPath(target_path); const sym_link_path_c = try toPosixPath(sym_link_path); @@ -1559,6 +1563,15 @@ pub fn symlink(target_path: []const u8, sym_link_path: []const u8) SymLinkError! pub const symlinkC = @compileError("deprecated: renamed to symlinkZ"); +/// Windows-only. Same as `symlink` except the parameters are null-terminated, WTF16 encoded. +/// 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, use `std.os.windows.CreateSymbolicLinkW` directly +/// specifying as flags `std.os.windows.CreateSymbolicLinkFlags.Directory`. +pub fn symlinkW(target_path: [*:0]const u16, sym_link_path: [*:0]const u16) SymLinkError!void { + const flags = windows.CreateSymbolicLinkFlags.File; + return windows.CreateSymbolicLinkW(sym_link_path, target_path, flags); +} + /// This is the same as `symlink` except the parameters are null-terminated pointers. /// See also `symlink`. pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8) SymLinkError!void { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 1ed1ef1f54ca..5439bc0df988 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -601,12 +601,17 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 { return buffer[0..end_index]; } -pub const CreateSymbolicLinkError = error{Unexpected}; +pub const CreateSymbolicLinkError = error{AccessDenied, FileNotFound, Unexpected}; + +pub const CreateSymbolicLinkFlags = enum(DWORD) { + File = SYMBOLIC_LINK_FLAG_FILE, + Directory = SYMBOLIC_LINK_FLAG_DIRECTORY, +}; pub fn CreateSymbolicLink( sym_link_path: []const u8, target_path: []const u8, - flags: DWORD, + flags: CreateSymbolicLinkFlags, ) CreateSymbolicLinkError!void { const sym_link_path_w = try sliceToPrefixedFileW(sym_link_path); const target_path_w = try sliceToPrefixedFileW(target_path); @@ -616,10 +621,32 @@ pub fn CreateSymbolicLink( pub fn CreateSymbolicLinkW( sym_link_path: [*:0]const u16, target_path: [*:0]const u16, - flags: DWORD, + flags: CreateSymbolicLinkFlags, ) CreateSymbolicLinkError!void { - if (kernel32.CreateSymbolicLinkW(sym_link_path, target_path, flags) == 0) { + // Previously, until Win 10 Creators Update, creating symbolic links required + // SeCreateSymbolicLink privilege. Currently, this is no longer required if the + // OS is in Developer Mode; however, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE + // must be added to the input flags. + if (kernel32.CreateSymbolicLinkW(sym_link_path, target_path, @enumToInt(flags) | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { switch (kernel32.GetLastError()) { + .INVALID_PARAMETER => { + // If we're on Windows pre Creators Update, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE + // flag is an invalid parameter, in which case repeat without the flag. + if (kernel32.CreateSymbolicLinkW(sym_link_path, target_path, @enumToInt(flags)) == 0) { + switch (kernel32.GetLastError()) { + .PRIVILEGE_NOT_HELD => return error.AccessDenied, + .FILE_NOT_FOUND => return error.FileNotFound, + .PATH_NOT_FOUND => return error.FileNotFound, + .ACCESS_DENIED => return error.AccessDenied, + else => |err| return unexpectedError(err), + } + } + return; + }, + .PRIVILEGE_NOT_HELD => return error.AccessDenied, + .FILE_NOT_FOUND => return error.FileNotFound, + .PATH_NOT_FOUND => return error.FileNotFound, + .ACCESS_DENIED => return error.AccessDenied, else => |err| return unexpectedError(err), } } diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index 998d4a809135..715a3cce7e63 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -1569,3 +1569,7 @@ pub const MAXIMUM_REPARSE_DATA_BUFFER_SIZE: usize = 16 * 1024; pub const FSCTL_GET_REPARSE_POINT: DWORD = 0x900a8; pub const IO_REPARSE_TAG_SYMLINK: ULONG = 0xa000000c; pub const IO_REPARSE_TAG_MOUNT_POINT: ULONG = 0xa0000003; + +pub const SYMBOLIC_LINK_FLAG_FILE: DWORD = 0x0; +pub const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1; +pub const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: DWORD = 0x2; \ No newline at end of file From 92d11fd4e95b86d6ace166783cda69030647e688 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 13 Jul 2020 17:51:14 +0200 Subject: [PATCH 04/28] Debug readlinkW using OpenFile --- lib/std/os.zig | 9 +++++---- lib/std/os/test.zig | 7 ++++--- lib/std/os/windows.zig | 23 ++++++++++++++++++++--- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 67b7dfe05b05..eb033babfb16 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1554,7 +1554,7 @@ pub fn symlink(target_path: []const u8, sym_link_path: []const u8) SymLinkError! if (builtin.os.tag == .windows) { const target_path_w = try windows.sliceToPrefixedFileW(target_path); const sym_link_path_w = try windows.sliceToPrefixedFileW(sym_link_path); - return symlinkW(sym_link_path_w.span().ptr, target_path_w.span().ptr); + return symlinkW(target_path_w.span().ptr, sym_link_path_w.span().ptr); } const target_path_c = try toPosixPath(target_path); const sym_link_path_c = try toPosixPath(sym_link_path); @@ -1578,7 +1578,7 @@ pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8) SymLin if (builtin.os.tag == .windows) { const target_path_w = try windows.cStrToPrefixedFileW(target_path); const sym_link_path_w = try windows.cStrToPrefixedFileW(sym_link_path); - return windows.CreateSymbolicLinkW(sym_link_path_w.span().ptr, target_path_w.span().ptr, 0); + return symlinkW(target_path_w.span().ptr, sym_link_path_w.span().ptr); } switch (errno(system.symlink(target_path, sym_link_path))) { 0 => return, @@ -2394,8 +2394,9 @@ pub const readlinkC = @compileError("deprecated: renamed to readlinkZ"); /// See also `readlinkZ`. pub fn readlinkW(file_path: []const u16, out_buffer: []u8) ReadLinkError![]u8 { const handle = windows.OpenFile(file_path, .{ - .access_mask = 0, - .creation = windows.FILE_OPEN_REPARSE_POINT | windows.FILE_LIST_DIRECTORY, + .access_mask = windows.GENERIC_READ, + .creation = windows.FILE_OPEN, + .options = windows.FILE_OPEN_REPARSE_POINT, .io_mode = std.io.default_mode, }) catch |err| { switch (err) { diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index b3f0136cf6af..eb2da384a6ba 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -45,7 +45,8 @@ test "readlink" { if (builtin.os.tag == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); - defer tmp.cleanup(); + //defer tmp.cleanup(); + std.debug.print("tmp = {}\n", .{tmp.sub_path[0..]}); // create file try tmp.dir.writeFile("file.txt", "nonsense"); @@ -59,8 +60,8 @@ test "readlink" { 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); }; - const target_path = try fs.path.join(&arena.allocator, &[_][]const u8{"file.txt"}); - const symlink_path = try fs.path.join(&arena.allocator, &[_][]const u8{"symlinked"}); + const target_path = try fs.path.join(&arena.allocator, &[_][]const u8{base_path, "file.txt"}); + const symlink_path = try fs.path.join(&arena.allocator, &[_][]const u8{base_path, "symlinked"}); // create symbolic link by path try os.symlink(target_path, symlink_path); diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 5439bc0df988..049a2a861234 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -110,6 +110,7 @@ pub const OpenFileOptions = struct { share_access: ULONG = FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, share_access_nonblocking: bool = false, creation: ULONG, + options: ?ULONG = null, io_mode: std.io.ModeOverride, }; @@ -145,7 +146,15 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN var delay: usize = 1; while (true) { - const blocking_flag: ULONG = if (options.io_mode == .blocking) FILE_SYNCHRONOUS_IO_NONALERT else 0; + var flags: ULONG = undefined; + if (options.options) |opt| { + flags = opt; + } else { + const blocking_flag: ULONG = if (options.io_mode == .blocking) FILE_SYNCHRONOUS_IO_NONALERT else 0; + flags = FILE_NON_DIRECTORY_FILE | blocking_flag; + } + // const blocking_flag: ULONG = if (options.io_mode == .blocking) FILE_SYNCHRONOUS_IO_NONALERT else 0; + // const flags = if (options.options) |opt| opt else FILE_NON_DIRECTORY_FILE; const rc = ntdll.NtCreateFile( &result, options.access_mask, @@ -155,7 +164,8 @@ 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 | blocking_flag, + flags, null, 0, ); @@ -601,7 +611,12 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 { return buffer[0..end_index]; } -pub const CreateSymbolicLinkError = error{AccessDenied, FileNotFound, Unexpected}; +pub const CreateSymbolicLinkError = error{ + AccessDenied, + PathAlreadyExists, + FileNotFound, + Unexpected +}; pub const CreateSymbolicLinkFlags = enum(DWORD) { File = SYMBOLIC_LINK_FLAG_FILE, @@ -638,6 +653,7 @@ pub fn CreateSymbolicLinkW( .FILE_NOT_FOUND => return error.FileNotFound, .PATH_NOT_FOUND => return error.FileNotFound, .ACCESS_DENIED => return error.AccessDenied, + .ALREADY_EXISTS => return error.PathAlreadyExists, else => |err| return unexpectedError(err), } } @@ -647,6 +663,7 @@ pub fn CreateSymbolicLinkW( .FILE_NOT_FOUND => return error.FileNotFound, .PATH_NOT_FOUND => return error.FileNotFound, .ACCESS_DENIED => return error.AccessDenied, + .ALREADY_EXISTS => return error.PathAlreadyExists, else => |err| return unexpectedError(err), } } From 49b58153642bbff19ab976b70e329c5592fa1684 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 13 Jul 2020 23:41:06 +0200 Subject: [PATCH 05/28] Add windows.ReadLink similar to OpenFile but for reparse points only --- lib/std/os.zig | 7 +---- lib/std/os/test.zig | 3 +- lib/std/os/windows.zig | 63 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index eb033babfb16..f9c50adc0633 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -2393,12 +2393,7 @@ pub const readlinkC = @compileError("deprecated: renamed to readlinkZ"); /// Windows-only. Same as `readlink` except `file_path` is null-terminated, WTF16 encoded. /// See also `readlinkZ`. pub fn readlinkW(file_path: []const u16, out_buffer: []u8) ReadLinkError![]u8 { - const handle = windows.OpenFile(file_path, .{ - .access_mask = windows.GENERIC_READ, - .creation = windows.FILE_OPEN, - .options = windows.FILE_OPEN_REPARSE_POINT, - .io_mode = std.io.default_mode, - }) catch |err| { + const handle = windows.ReadLink(file_path) catch |err| { switch (err) { error.IsDir => unreachable, error.NoDevice => return error.FileNotFound, diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index eb2da384a6ba..826be1cc8e66 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -45,8 +45,7 @@ test "readlink" { if (builtin.os.tag == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); - //defer tmp.cleanup(); - std.debug.print("tmp = {}\n", .{tmp.sub_path[0..]}); + defer tmp.cleanup(); // create file try tmp.dir.writeFile("file.txt", "nonsense"); diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 049a2a861234..9fc0a123befa 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -110,7 +110,6 @@ pub const OpenFileOptions = struct { share_access: ULONG = FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, share_access_nonblocking: bool = false, creation: ULONG, - options: ?ULONG = null, io_mode: std.io.ModeOverride, }; @@ -147,14 +146,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN var delay: usize = 1; while (true) { var flags: ULONG = undefined; - if (options.options) |opt| { - flags = opt; - } else { - const blocking_flag: ULONG = if (options.io_mode == .blocking) FILE_SYNCHRONOUS_IO_NONALERT else 0; - flags = FILE_NON_DIRECTORY_FILE | blocking_flag; - } - // const blocking_flag: ULONG = if (options.io_mode == .blocking) FILE_SYNCHRONOUS_IO_NONALERT else 0; - // const flags = if (options.options) |opt| opt else FILE_NON_DIRECTORY_FILE; + const blocking_flag: ULONG = if (options.io_mode == .blocking) FILE_SYNCHRONOUS_IO_NONALERT else 0; const rc = ntdll.NtCreateFile( &result, options.access_mask, @@ -164,8 +156,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN FILE_ATTRIBUTE_NORMAL, options.share_access, options.creation, - // flags | blocking_flag, - flags, + FILE_NON_DIRECTORY_FILE | blocking_flag, null, 0, ); @@ -1380,3 +1371,53 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError { } return error.Unexpected; } + +pub fn ReadLink(path_w: []const u16) OpenError!HANDLE { + var result: HANDLE = undefined; + + const path_len_bytes = math.cast(u16, 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(path_w.ptr)), + }; + var attr = OBJECT_ATTRIBUTES{ + .Length = @sizeOf(OBJECT_ATTRIBUTES), + .RootDirectory = null, + .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( + &result, + FILE_LIST_DIRECTORY, + &attr, + &io, + null, + FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_DELETE, + FILE_OPEN, + FILE_OPEN_REPARSE_POINT, + null, + 0, + ); + switch (rc) { + .SUCCESS => return result, + .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, + .SHARING_VIOLATION => return error.WouldBlock, + .ACCESS_DENIED => return error.AccessDenied, + .PIPE_BUSY => return error.PipeBusy, + .OBJECT_PATH_SYNTAX_BAD => unreachable, + .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, + .FILE_IS_A_DIRECTORY => return error.IsDir, + else => return unexpectedStatus(rc), + } +} \ No newline at end of file From 9b00dc941bf641f171443d18466b5fc9cbcc882b Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 14 Jul 2020 08:01:04 +0200 Subject: [PATCH 06/28] Use windows.CreateFileW to open the reparse point --- lib/std/os.zig | 28 +++++++++++---------- lib/std/os/windows.zig | 50 ------------------------------------- lib/std/os/windows/bits.zig | 4 +-- 3 files changed, 17 insertions(+), 65 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index f9c50adc0633..75254b52c84e 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -2381,7 +2381,7 @@ pub fn readlink(file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 { @compileError("readlink is not supported in WASI; use readlinkat instead"); } else if (builtin.os.tag == .windows) { const file_path_w = try windows.sliceToPrefixedFileW(file_path); - return readlinkW(file_path_w.span(), out_buffer); + return readlinkW(file_path_w.span().ptr, out_buffer); } else { const file_path_c = try toPosixPath(file_path); return readlinkZ(&file_path_c, out_buffer); @@ -2392,26 +2392,28 @@ pub const readlinkC = @compileError("deprecated: renamed to readlinkZ"); /// Windows-only. Same as `readlink` except `file_path` is null-terminated, WTF16 encoded. /// See also `readlinkZ`. -pub fn readlinkW(file_path: []const u16, out_buffer: []u8) ReadLinkError![]u8 { - const handle = windows.ReadLink(file_path) catch |err| { +pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { + const w = windows; + const access_mode: w.DWORD = 0; + const sharing = w.FILE_SHARE_DELETE | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE; + const disposition = w.OPEN_EXISTING; + const flags = w.FILE_FLAG_BACKUP_SEMANTICS | w.FILE_FLAG_OPEN_REPARSE_POINT; + const handle = w.CreateFileW(file_path, access_mode, sharing, null, disposition, flags, null) catch |err| { switch (err) { - error.IsDir => unreachable, - error.NoDevice => return error.FileNotFound, error.SharingViolation => return error.AccessDenied, - error.PipeBusy => unreachable, error.PathAlreadyExists => unreachable, - error.WouldBlock => unreachable, + error.PipeBusy => unreachable, else => |e| return e, } }; - var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; - _ = try windows.DeviceIoControl(handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); - const reparse_struct = mem.bytesAsValue(windows._REPARSE_DATA_BUFFER, reparse_buf[0..@sizeOf(windows._REPARSE_DATA_BUFFER)]); + var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; + _ = try w.DeviceIoControl(handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); + const reparse_struct = mem.bytesAsValue(w.REPARSE_DATA_BUFFER, reparse_buf[0..@sizeOf(w.REPARSE_DATA_BUFFER)]); switch (reparse_struct.ReparseTag) { - windows.IO_REPARSE_TAG_SYMLINK => { + w.IO_REPARSE_TAG_SYMLINK => { std.debug.warn("got symlink!", .{}); }, - windows.IO_REPARSE_TAG_MOUNT_POINT => { + w.IO_REPARSE_TAG_MOUNT_POINT => { std.debug.warn("got mount point!", .{}); }, else => |value| { @@ -2426,7 +2428,7 @@ pub fn readlinkW(file_path: []const u16, out_buffer: []u8) ReadLinkError![]u8 { pub fn readlinkZ(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 readlinkW(file_path_w.span(), out_buffer); + return readlinkW(file_path_w.span().ptr, out_buffer); } const rc = system.readlink(file_path, out_buffer.ptr, out_buffer.len); switch (errno(rc)) { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 9fc0a123befa..0a850a25c42b 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1370,54 +1370,4 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError { std.debug.dumpCurrentStackTrace(null); } return error.Unexpected; -} - -pub fn ReadLink(path_w: []const u16) OpenError!HANDLE { - var result: HANDLE = undefined; - - const path_len_bytes = math.cast(u16, 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(path_w.ptr)), - }; - var attr = OBJECT_ATTRIBUTES{ - .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = null, - .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( - &result, - FILE_LIST_DIRECTORY, - &attr, - &io, - null, - FILE_ATTRIBUTE_NORMAL, - FILE_SHARE_READ | FILE_SHARE_DELETE, - FILE_OPEN, - FILE_OPEN_REPARSE_POINT, - null, - 0, - ); - switch (rc) { - .SUCCESS => return result, - .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, - .SHARING_VIOLATION => return error.WouldBlock, - .ACCESS_DENIED => return error.AccessDenied, - .PIPE_BUSY => return error.PipeBusy, - .OBJECT_PATH_SYNTAX_BAD => unreachable, - .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - .FILE_IS_A_DIRECTORY => return error.IsDir, - else => return unexpectedStatus(rc), - } } \ No newline at end of file diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index 715a3cce7e63..fef9af9bdaa8 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -1543,7 +1543,7 @@ pub const LPOSVERSIONINFOW = *OSVERSIONINFOW; pub const RTL_OSVERSIONINFOW = OSVERSIONINFOW; pub const PRTL_OSVERSIONINFOW = *RTL_OSVERSIONINFOW; -pub const _REPARSE_DATA_BUFFER = extern struct { +pub const REPARSE_DATA_BUFFER = extern struct { ReparseTag: ULONG, ReparseDataLength: USHORT, Reserved: USHORT, u: extern union { SymbolicLinkReparseBuffer: extern struct { SubstituteNameOffset: USHORT, @@ -1572,4 +1572,4 @@ pub const IO_REPARSE_TAG_MOUNT_POINT: ULONG = 0xa0000003; pub const SYMBOLIC_LINK_FLAG_FILE: DWORD = 0x0; pub const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1; -pub const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: DWORD = 0x2; \ No newline at end of file +pub const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: DWORD = 0x2; From d17c9b3591ba822df3af341a0d4cf5f004413f4a Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 14 Jul 2020 08:45:04 +0200 Subject: [PATCH 07/28] Fix incorrect byte format of REPARSE_DATA_BUFFER struct --- lib/std/os.zig | 22 ++++++++++++------- lib/std/os/windows/bits.zig | 42 ++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 75254b52c84e..24de1d83f2d0 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -2394,11 +2394,10 @@ pub const readlinkC = @compileError("deprecated: renamed to readlinkZ"); /// See also `readlinkZ`. pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { const w = windows; - const access_mode: w.DWORD = 0; const sharing = w.FILE_SHARE_DELETE | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE; const disposition = w.OPEN_EXISTING; const flags = w.FILE_FLAG_BACKUP_SEMANTICS | w.FILE_FLAG_OPEN_REPARSE_POINT; - const handle = w.CreateFileW(file_path, access_mode, sharing, null, disposition, flags, null) catch |err| { + const handle = w.CreateFileW(file_path, 0, sharing, null, disposition, flags, null) catch |err| { switch (err) { error.SharingViolation => return error.AccessDenied, error.PathAlreadyExists => unreachable, @@ -2406,22 +2405,31 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 else => |e| return e, } }; - var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; + var reparse_buf align(@alignOf(w.REPARSE_DATA_BUFFER)) = [_]u8{0} ** (w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); _ = try w.DeviceIoControl(handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); - const reparse_struct = mem.bytesAsValue(w.REPARSE_DATA_BUFFER, reparse_buf[0..@sizeOf(w.REPARSE_DATA_BUFFER)]); + const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, &reparse_buf[0]); switch (reparse_struct.ReparseTag) { w.IO_REPARSE_TAG_SYMLINK => { - std.debug.warn("got symlink!", .{}); + const alignment = @alignOf(w.SymbolicLinkReparseBuffer); + const buf = @ptrCast(*const w.SymbolicLinkReparseBuffer, @alignCast(alignment, &reparse_struct.DataBuffer[0])); + const offset = buf.SubstituteNameOffset / 2; + const len = buf.SubstituteNameLength / 2; + const f = buf.Flags; + const path_buf = @as([*]const u16, &buf.PathBuffer); + std.debug.warn("got symlink => offset={}, len={}, flags = {}, {}\n", .{ offset, len, f, w.SYMLINK_FLAG_RELATIVE }); + // TODO handle absolute paths and namespace prefix + const out_len = std.unicode.utf16leToUtf8(out_buffer, path_buf[offset .. offset + len]) catch unreachable; + std.debug.warn("got symlink => utf8={}\n", .{out_buffer[0..out_len]}); + return out_buffer[0..out_len]; }, w.IO_REPARSE_TAG_MOUNT_POINT => { - std.debug.warn("got mount point!", .{}); + @panic("TODO parse mount point"); }, else => |value| { std.debug.warn("unsupported symlink type: {}", .{value}); return error.UnsupportedSymlinkType; }, } - @panic("Oh no!"); } /// Same as `readlink` except `file_path` is null-terminated. diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index fef9af9bdaa8..66ea5bb51240 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -1544,31 +1544,31 @@ pub const RTL_OSVERSIONINFOW = OSVERSIONINFOW; pub const PRTL_OSVERSIONINFOW = *RTL_OSVERSIONINFOW; pub const REPARSE_DATA_BUFFER = extern struct { - ReparseTag: ULONG, ReparseDataLength: USHORT, Reserved: USHORT, u: extern union { - SymbolicLinkReparseBuffer: extern struct { - SubstituteNameOffset: USHORT, - SubstituteNameLength: USHORT, - PrintNameOffset: USHORT, - PrintNameLength: USHORT, - Flags: ULONG, - PathBuffer: [1]WCHAR, - }, - MountPointReparseBuffer: extern struct { - SubstituteNameOffset: USHORT, - SubstituteNameLength: USHORT, - PrintNameOffset: USHORT, - PrintNameLength: USHORT, - PathBuffer: [1]WCHAR, - }, - GenericReparseBuffer: extern struct { - DataBuffer: [1]UCHAR, - }, - } + ReparseTag: ULONG, + ReparseDataLength: USHORT, + Reserved: USHORT, + DataBuffer: [1]UCHAR, +}; +pub const SymbolicLinkReparseBuffer = extern struct { + SubstituteNameOffset: USHORT, + SubstituteNameLength: USHORT, + PrintNameOffset: USHORT, + PrintNameLength: USHORT, + Flags: ULONG, + PathBuffer: [1]WCHAR, +}; +pub const MountPointReparseBuffer = extern struct { + SubstituteNameOffset: USHORT, + SubstituteNameLength: USHORT, + PrintNameOffset: USHORT, + PrintNameLength: USHORT, + PathBuffer: [1]WCHAR, }; -pub const MAXIMUM_REPARSE_DATA_BUFFER_SIZE: usize = 16 * 1024; +pub const MAXIMUM_REPARSE_DATA_BUFFER_SIZE: ULONG = 16 * 1024; pub const FSCTL_GET_REPARSE_POINT: DWORD = 0x900a8; pub const IO_REPARSE_TAG_SYMLINK: ULONG = 0xa000000c; pub const IO_REPARSE_TAG_MOUNT_POINT: ULONG = 0xa0000003; +pub const SYMLINK_FLAG_RELATIVE: ULONG = 0x1; pub const SYMBOLIC_LINK_FLAG_FILE: DWORD = 0x0; pub const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1; From ae8abedbeda33ff5cd93ca2fb21ef2f5453dfb37 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 14 Jul 2020 23:06:55 +0200 Subject: [PATCH 08/28] Use NtCreateFile to get handle to reparse point --- lib/std/os.zig | 44 +++++++++++++---------- lib/std/os/test.zig | 58 +++++++++++++++++------------- lib/std/os/windows.zig | 70 +++++++++++++++++++++++++++++++++++++ lib/std/os/windows/bits.zig | 2 +- 4 files changed, 131 insertions(+), 43 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 24de1d83f2d0..e3e44b6a3008 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -2394,36 +2394,37 @@ pub const readlinkC = @compileError("deprecated: renamed to readlinkZ"); /// See also `readlinkZ`. pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { const w = windows; - const sharing = w.FILE_SHARE_DELETE | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE; - const disposition = w.OPEN_EXISTING; - const flags = w.FILE_FLAG_BACKUP_SEMANTICS | w.FILE_FLAG_OPEN_REPARSE_POINT; - const handle = w.CreateFileW(file_path, 0, sharing, null, disposition, flags, null) catch |err| { + + const dir = if (std.fs.path.isAbsoluteWindowsW(file_path)) null else std.fs.cwd().fd; + const handle = w.OpenAsReparsePoint(dir, file_path) catch |err| { switch (err) { error.SharingViolation => return error.AccessDenied, - error.PathAlreadyExists => unreachable, error.PipeBusy => unreachable, + error.PathAlreadyExists => unreachable, + error.NoDevice => return error.FileNotFound, else => |e| return e, } }; - var reparse_buf align(@alignOf(w.REPARSE_DATA_BUFFER)) = [_]u8{0} ** (w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); + + var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; _ = try w.DeviceIoControl(handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); - const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, &reparse_buf[0]); + // std.debug.warn("\n\n{x}\n\n", .{reparse_buf}); + const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, @alignCast(@alignOf(w.REPARSE_DATA_BUFFER), &reparse_buf[0])); switch (reparse_struct.ReparseTag) { w.IO_REPARSE_TAG_SYMLINK => { - const alignment = @alignOf(w.SymbolicLinkReparseBuffer); - const buf = @ptrCast(*const w.SymbolicLinkReparseBuffer, @alignCast(alignment, &reparse_struct.DataBuffer[0])); - const offset = buf.SubstituteNameOffset / 2; - const len = buf.SubstituteNameLength / 2; - const f = buf.Flags; + const buf = @ptrCast(*const w.SymbolicLinkReparseBuffer, @alignCast(@alignOf(w.SymbolicLinkReparseBuffer), &reparse_struct.DataBuffer[0])); + const offset = buf.SubstituteNameOffset >> 1; + const len = buf.SubstituteNameLength >> 1; const path_buf = @as([*]const u16, &buf.PathBuffer); - std.debug.warn("got symlink => offset={}, len={}, flags = {}, {}\n", .{ offset, len, f, w.SYMLINK_FLAG_RELATIVE }); - // TODO handle absolute paths and namespace prefix - const out_len = std.unicode.utf16leToUtf8(out_buffer, path_buf[offset .. offset + len]) catch unreachable; - std.debug.warn("got symlink => utf8={}\n", .{out_buffer[0..out_len]}); - return out_buffer[0..out_len]; + const is_relative = buf.Flags & w.SYMLINK_FLAG_RELATIVE != 0; + return parseReadlinkPath(path_buf[offset .. offset + len], is_relative, out_buffer); }, w.IO_REPARSE_TAG_MOUNT_POINT => { - @panic("TODO parse mount point"); + const buf = @ptrCast(*const w.MountPointReparseBuffer, @alignCast(@alignOf(w.MountPointReparseBuffer), &reparse_struct.DataBuffer[0])); + const offset = buf.SubstituteNameOffset >> 1; + const len = buf.SubstituteNameLength >> 1; + const path_buf = @as([*]const u16, &buf.PathBuffer); + return parseReadlinkPath(path_buf[offset .. offset + len], false, out_buffer); }, else => |value| { std.debug.warn("unsupported symlink type: {}", .{value}); @@ -2432,6 +2433,13 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 } } +fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 { + const out_len = std.unicode.utf16leToUtf8(out_buffer, path) catch unreachable; + std.debug.warn("got symlink => utf8={}\n", .{out_buffer[0..out_len]}); + // TODO handle absolute paths and namespace prefix '/??/' + return out_buffer[0..out_len]; +} + /// 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) { diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 826be1cc8e66..a444df3e87f1 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -43,32 +43,42 @@ test "fstatat" { test "readlink" { if (builtin.os.tag == .wasi) return error.SkipZigTest; + + var cwd = fs.cwd(); + try cwd.writeFile("file.txt", "nonsense"); + try os.symlink("file.txt", "symlinked"); - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - // create file - try tmp.dir.writeFile("file.txt", "nonsense"); - - // get paths - // TODO: use Dir's realpath function once that exists - 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); - }; - const target_path = try fs.path.join(&arena.allocator, &[_][]const u8{base_path, "file.txt"}); - const symlink_path = try fs.path.join(&arena.allocator, &[_][]const u8{base_path, "symlinked"}); - - // create symbolic link by path - try os.symlink(target_path, symlink_path); - - // now, read the link and verify var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; - const given = try os.readlink(symlink_path, buffer[0..]); - expect(mem.eql(u8, symlink_path, given)); + const given = try os.readlink("symlinked", buffer[0..]); + expect(mem.eql(u8, "file.txt", given)); + + // var tmp = tmpDir(.{}); + // defer tmp.cleanup(); + + // // create file + // try tmp.dir.writeFile("file.txt", "nonsense"); + + // // get paths + // // TODO: use Dir's realpath function once that exists + // 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); + // }; + // const target_path = try fs.path.join(&arena.allocator, &[_][]const u8{base_path, "file.txt"}); + // const symlink_path = try fs.path.join(&arena.allocator, &[_][]const u8{base_path, "symlinked"}); + // std.debug.warn("\ntarget_path={}\n", .{target_path}); + // std.debug.warn("symlink_path={}\n", .{symlink_path}); + + // // create symbolic link by path + // try os.symlink(target_path, symlink_path); + + // // now, read the link and verify + // var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + // const given = try os.readlink(symlink_path, buffer[0..]); + // expect(mem.eql(u8, symlink_path, given)); } test "readlinkat" { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 0a850a25c42b..d6d1b2db84d6 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1370,4 +1370,74 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError { std.debug.dumpCurrentStackTrace(null); } return error.Unexpected; +} + +pub const OpenAsReparsePointError = error { + FileNotFound, + NoDevice, + SharingViolation, + AccessDenied, + PipeBusy, + PathAlreadyExists, + Unexpected, + NameTooLong, +}; + +/// Open file as a reparse point +pub fn OpenAsReparsePoint( + dir: ?HANDLE, + sub_path_w: [*:0]const u16, +) OpenAsReparsePointError!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 = null, + .SecurityQualityOfService = null, + }; + var io: IO_STATUS_BLOCK = undefined; + var result_handle: HANDLE = undefined; + const rc = ntdll.NtCreateFile( + &result_handle, + FILE_READ_ATTRIBUTES, + &attr, + &io, + null, + FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ, + FILE_OPEN, + FILE_OPEN_REPARSE_POINT, + 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, + .SHARING_VIOLATION => return error.SharingViolation, + .ACCESS_DENIED => return error.AccessDenied, + .PIPE_BUSY => return error.PipeBusy, + .OBJECT_PATH_SYNTAX_BAD => unreachable, + .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, + .FILE_IS_A_DIRECTORY => unreachable, + else => return unexpectedStatus(rc), + } } \ No newline at end of file diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index 66ea5bb51240..8d3043f76ff3 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -1568,7 +1568,7 @@ pub const MAXIMUM_REPARSE_DATA_BUFFER_SIZE: ULONG = 16 * 1024; pub const FSCTL_GET_REPARSE_POINT: DWORD = 0x900a8; pub const IO_REPARSE_TAG_SYMLINK: ULONG = 0xa000000c; pub const IO_REPARSE_TAG_MOUNT_POINT: ULONG = 0xa0000003; -pub const SYMLINK_FLAG_RELATIVE: ULONG = 0x1; +pub const SYMLINK_FLAG_RELATIVE: ULONG = 0x00000001; pub const SYMBOLIC_LINK_FLAG_FILE: DWORD = 0x0; pub const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1; From c47cb8d09f7cf1c02d3af59ebbca665ce78c85ca Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 14 Jul 2020 23:30:05 +0200 Subject: [PATCH 09/28] Fix unlinkatW to allow file symlink deletion on Windows --- lib/std/os.zig | 10 ++--- lib/std/os/test.zig | 79 +++++++++++++++++++++---------------- lib/std/os/windows/bits.zig | 4 +- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index e3e44b6a3008..7b8587144631 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1834,7 +1834,7 @@ pub fn unlinkatW(dirfd: fd_t, sub_path_w: [*:0]const u16, flags: u32) UnlinkatEr const create_options_flags = if (want_rmdir_behavior) @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_DIRECTORY_FILE) else - @as(w.ULONG, w.FILE_DELETE_ON_CLOSE | w.FILE_NON_DIRECTORY_FILE); + @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{ @@ -2371,7 +2371,7 @@ pub const ReadLinkError = error{ InvalidUtf8, BadPathName, /// Windows-only. - UnsupportedSymlinkType, + UnsupportedReparsePointType, } || UnexpectedError; /// Read value of a symbolic link. @@ -2412,7 +2412,7 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, @alignCast(@alignOf(w.REPARSE_DATA_BUFFER), &reparse_buf[0])); switch (reparse_struct.ReparseTag) { w.IO_REPARSE_TAG_SYMLINK => { - const buf = @ptrCast(*const w.SymbolicLinkReparseBuffer, @alignCast(@alignOf(w.SymbolicLinkReparseBuffer), &reparse_struct.DataBuffer[0])); + const buf = @ptrCast(*const w.SYMBOLIC_LINK_REPARSE_BUFFER, @alignCast(@alignOf(w.SYMBOLIC_LINK_REPARSE_BUFFER), &reparse_struct.DataBuffer[0])); const offset = buf.SubstituteNameOffset >> 1; const len = buf.SubstituteNameLength >> 1; const path_buf = @as([*]const u16, &buf.PathBuffer); @@ -2420,7 +2420,7 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 return parseReadlinkPath(path_buf[offset .. offset + len], is_relative, out_buffer); }, w.IO_REPARSE_TAG_MOUNT_POINT => { - const buf = @ptrCast(*const w.MountPointReparseBuffer, @alignCast(@alignOf(w.MountPointReparseBuffer), &reparse_struct.DataBuffer[0])); + const buf = @ptrCast(*const w.MOUNT_POINT_REPARSE_BUFFER, @alignCast(@alignOf(w.MOUNT_POINT_REPARSE_BUFFER), &reparse_struct.DataBuffer[0])); const offset = buf.SubstituteNameOffset >> 1; const len = buf.SubstituteNameLength >> 1; const path_buf = @as([*]const u16, &buf.PathBuffer); @@ -2428,7 +2428,7 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 }, else => |value| { std.debug.warn("unsupported symlink type: {}", .{value}); - return error.UnsupportedSymlinkType; + return error.UnsupportedReparsePointType; }, } } diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index a444df3e87f1..6b9c19c4841d 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -43,42 +43,51 @@ test "fstatat" { test "readlink" { if (builtin.os.tag == .wasi) return error.SkipZigTest; - - var cwd = fs.cwd(); - try cwd.writeFile("file.txt", "nonsense"); - try os.symlink("file.txt", "symlinked"); - var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; - const given = try os.readlink("symlinked", buffer[0..]); - expect(mem.eql(u8, "file.txt", given)); - - // var tmp = tmpDir(.{}); - // defer tmp.cleanup(); - - // // create file - // try tmp.dir.writeFile("file.txt", "nonsense"); - - // // get paths - // // TODO: use Dir's realpath function once that exists - // 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); - // }; - // const target_path = try fs.path.join(&arena.allocator, &[_][]const u8{base_path, "file.txt"}); - // const symlink_path = try fs.path.join(&arena.allocator, &[_][]const u8{base_path, "symlinked"}); - // std.debug.warn("\ntarget_path={}\n", .{target_path}); - // std.debug.warn("symlink_path={}\n", .{symlink_path}); - - // // create symbolic link by path - // try os.symlink(target_path, symlink_path); - - // // now, read the link and verify - // var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; - // const given = try os.readlink(symlink_path, buffer[0..]); - // expect(mem.eql(u8, symlink_path, given)); + // First, try relative paths + { + var cwd = fs.cwd(); + try cwd.writeFile("file.txt", "nonsense"); + try os.symlink("file.txt", "symlinked"); + + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const given = try os.readlink("symlinked", buffer[0..]); + expect(mem.eql(u8, "file.txt", given)); + + try cwd.deleteFile("file.txt"); + try cwd.deleteFile("symlinked"); + } + + // Next, let's try fully-qualified paths + { + var tmp = tmpDir(.{}); + // defer tmp.cleanup(); + + // create file + try tmp.dir.writeFile("file.txt", "nonsense"); + + // get paths + // TODO: use Dir's realpath function once that exists + 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); + }; + const target_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "file.txt" }); + const symlink_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "symlinked" }); + std.debug.warn("\ntarget_path={}\n", .{target_path}); + std.debug.warn("symlink_path={}\n", .{symlink_path}); + + // create symbolic link by path + try os.symlink(target_path, symlink_path); + + // now, read the link and verify + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const given = try os.readlink(symlink_path, buffer[0..]); + expect(mem.eql(u8, symlink_path, given)); + } } test "readlinkat" { diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index 8d3043f76ff3..1c5e1a3f7088 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -1549,7 +1549,7 @@ pub const REPARSE_DATA_BUFFER = extern struct { Reserved: USHORT, DataBuffer: [1]UCHAR, }; -pub const SymbolicLinkReparseBuffer = extern struct { +pub const SYMBOLIC_LINK_REPARSE_BUFFER = extern struct { SubstituteNameOffset: USHORT, SubstituteNameLength: USHORT, PrintNameOffset: USHORT, @@ -1557,7 +1557,7 @@ pub const SymbolicLinkReparseBuffer = extern struct { Flags: ULONG, PathBuffer: [1]WCHAR, }; -pub const MountPointReparseBuffer = extern struct { +pub const MOUNT_POINT_REPARSE_BUFFER = extern struct { SubstituteNameOffset: USHORT, SubstituteNameLength: USHORT, PrintNameOffset: USHORT, From 99e3e29e2e611cf86941921770644401a3cf74b7 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 15 Jul 2020 00:24:16 +0200 Subject: [PATCH 10/28] Refactor --- lib/std/os.zig | 29 ++++++++++++++++++++--------- lib/std/os/test.zig | 1 + lib/std/os/windows.zig | 31 +++++++++++-------------------- lib/std/os/windows/bits.zig | 3 +-- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 7b8587144631..cfcab92bd4f1 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1568,8 +1568,7 @@ pub const symlinkC = @compileError("deprecated: renamed to symlinkZ"); /// like to create a symbolic link to a directory, use `std.os.windows.CreateSymbolicLinkW` directly /// specifying as flags `std.os.windows.CreateSymbolicLinkFlags.Directory`. pub fn symlinkW(target_path: [*:0]const u16, sym_link_path: [*:0]const u16) SymLinkError!void { - const flags = windows.CreateSymbolicLinkFlags.File; - return windows.CreateSymbolicLinkW(sym_link_path, target_path, flags); + return windows.CreateSymbolicLinkW(sym_link_path, target_path, false); } /// This is the same as `symlink` except the parameters are null-terminated pointers. @@ -2395,20 +2394,20 @@ pub const readlinkC = @compileError("deprecated: renamed to readlinkZ"); pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { const w = windows; - const dir = if (std.fs.path.isAbsoluteWindowsW(file_path)) null else std.fs.cwd().fd; - const handle = w.OpenAsReparsePoint(dir, file_path) catch |err| { + const sharing = w.FILE_SHARE_DELETE | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE; + const disposition = w.OPEN_EXISTING; + const flags = w.FILE_FLAG_BACKUP_SEMANTICS | w.FILE_FLAG_OPEN_REPARSE_POINT; + const handle = w.CreateFileW(file_path, 0, sharing, null, disposition, flags, null) catch |err| { switch (err) { error.SharingViolation => return error.AccessDenied, error.PipeBusy => unreachable, error.PathAlreadyExists => unreachable, - error.NoDevice => return error.FileNotFound, else => |e| return e, } }; var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; _ = try w.DeviceIoControl(handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); - // std.debug.warn("\n\n{x}\n\n", .{reparse_buf}); const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, @alignCast(@alignOf(w.REPARSE_DATA_BUFFER), &reparse_buf[0])); switch (reparse_struct.ReparseTag) { w.IO_REPARSE_TAG_SYMLINK => { @@ -2434,9 +2433,9 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 } fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 { - const out_len = std.unicode.utf16leToUtf8(out_buffer, path) catch unreachable; - std.debug.warn("got symlink => utf8={}\n", .{out_buffer[0..out_len]}); - // TODO handle absolute paths and namespace prefix '/??/' + const prefix = [_]u16{ '\\', '?', '?', '\\' }; + const start_index = if (mem.startsWith(u16, path, &prefix)) prefix.len else 0; + const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable; return out_buffer[0..out_len]; } @@ -2502,6 +2501,18 @@ 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 { + const w = windows; + + const handle = w.OpenReparsePoint(dir, file_path) catch |err| { + switch (err) { + error.SharingViolation => return error.AccessDenied, + error.PathAlreadyExists => unreachable, + error.PipeBusy => unreachable, + error.PathAlreadyExists => unreachable, + error.NoDevice => return error.FileNotFound, + else => |e| return e, + } + }; @compileError("TODO implement on Windows"); } diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 6b9c19c4841d..3ef1006b0a5f 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -86,6 +86,7 @@ test "readlink" { // now, read the link and verify var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; const given = try os.readlink(symlink_path, buffer[0..]); + std.debug.warn("given={}\n", .{given}); expect(mem.eql(u8, symlink_path, given)); } } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index d6d1b2db84d6..67f200d3395f 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -602,43 +602,34 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 { return buffer[0..end_index]; } -pub const CreateSymbolicLinkError = error{ - AccessDenied, - PathAlreadyExists, - FileNotFound, - Unexpected -}; - -pub const CreateSymbolicLinkFlags = enum(DWORD) { - File = SYMBOLIC_LINK_FLAG_FILE, - Directory = SYMBOLIC_LINK_FLAG_DIRECTORY, -}; +pub const CreateSymbolicLinkError = error{ AccessDenied, PathAlreadyExists, FileNotFound, Unexpected }; pub fn CreateSymbolicLink( sym_link_path: []const u8, target_path: []const u8, - flags: CreateSymbolicLinkFlags, + 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(sym_link_path_w.span().ptr, target_path_w.span().ptr, flags); + return CreateSymbolicLinkW(sym_link_path_w.span().ptr, target_path_w.span().ptr, is_directory); } pub fn CreateSymbolicLinkW( sym_link_path: [*:0]const u16, target_path: [*:0]const u16, - flags: CreateSymbolicLinkFlags, + is_directory: bool, ) CreateSymbolicLinkError!void { // Previously, until Win 10 Creators Update, creating symbolic links required // SeCreateSymbolicLink privilege. Currently, this is no longer required if the // OS is in Developer Mode; however, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE // must be added to the input flags. - if (kernel32.CreateSymbolicLinkW(sym_link_path, target_path, @enumToInt(flags) | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + const flags = if (is_directory) SYMBOLIC_LINK_FLAG_DIRECTORY else 0; + if (kernel32.CreateSymbolicLinkW(sym_link_path, target_path, flags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { switch (kernel32.GetLastError()) { .INVALID_PARAMETER => { // If we're on Windows pre Creators Update, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE // flag is an invalid parameter, in which case repeat without the flag. - if (kernel32.CreateSymbolicLinkW(sym_link_path, target_path, @enumToInt(flags)) == 0) { + if (kernel32.CreateSymbolicLinkW(sym_link_path, target_path, flags) == 0) { switch (kernel32.GetLastError()) { .PRIVILEGE_NOT_HELD => return error.AccessDenied, .FILE_NOT_FOUND => return error.FileNotFound, @@ -1372,7 +1363,7 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError { return error.Unexpected; } -pub const OpenAsReparsePointError = error { +pub const OpenReparsePointError = error{ FileNotFound, NoDevice, SharingViolation, @@ -1384,10 +1375,10 @@ pub const OpenAsReparsePointError = error { }; /// Open file as a reparse point -pub fn OpenAsReparsePoint( +pub fn OpenReparsePoint( dir: ?HANDLE, sub_path_w: [*:0]const u16, -) OpenAsReparsePointError!HANDLE { +) OpenReparsePointError!HANDLE { const path_len_bytes = math.cast(u16, mem.lenZ(sub_path_w) * 2) catch |err| switch (err) { error.Overflow => return error.NameTooLong, }; @@ -1440,4 +1431,4 @@ pub fn OpenAsReparsePoint( .FILE_IS_A_DIRECTORY => unreachable, else => return unexpectedStatus(rc), } -} \ No newline at end of file +} diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index 1c5e1a3f7088..7295b6a404d3 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -1568,8 +1568,7 @@ pub const MAXIMUM_REPARSE_DATA_BUFFER_SIZE: ULONG = 16 * 1024; pub const FSCTL_GET_REPARSE_POINT: DWORD = 0x900a8; pub const IO_REPARSE_TAG_SYMLINK: ULONG = 0xa000000c; pub const IO_REPARSE_TAG_MOUNT_POINT: ULONG = 0xa0000003; -pub const SYMLINK_FLAG_RELATIVE: ULONG = 0x00000001; +pub const SYMLINK_FLAG_RELATIVE: ULONG = 0x1; -pub const SYMBOLIC_LINK_FLAG_FILE: DWORD = 0x0; pub const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1; pub const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: DWORD = 0x2; From 4894de2b326363c9a157411bf555d73b661c74c4 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 15 Jul 2020 00:28:45 +0200 Subject: [PATCH 11/28] Fix readlink smoke test --- lib/std/os/test.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 3ef1006b0a5f..597ec88951ad 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -87,7 +87,7 @@ test "readlink" { var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; const given = try os.readlink(symlink_path, buffer[0..]); std.debug.warn("given={}\n", .{given}); - expect(mem.eql(u8, symlink_path, given)); + expect(mem.eql(u8, target_path, given)); } } From 30f1176a545111e8bdce3362d6bdbf7ef75dce2a Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 15 Jul 2020 09:06:01 +0200 Subject: [PATCH 12/28] Add SymlinkFlags needed to create symlinks to dirs on Win --- lib/std/fs.zig | 4 ++-- lib/std/os.zig | 27 +++++++++++++++------------ lib/std/os/test.zig | 4 ++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 0feaf69d6780..6b4c09845c78 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -69,7 +69,7 @@ pub const need_async_thread = std.io.is_async and switch (builtin.os.tag) { /// TODO remove the allocator requirement from this API pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: []const u8) !void { - if (symLink(existing_path, new_path)) { + if (symLink(existing_path, new_path, .{})) { return; } else |err| switch (err) { error.PathAlreadyExists => {}, @@ -87,7 +87,7 @@ pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: try crypto.randomBytes(rand_buf[0..]); base64_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf); - if (symLink(existing_path, tmp_path)) { + if (symLink(existing_path, tmp_path, .{})) { return rename(tmp_path, new_path); } else |err| switch (err) { error.PathAlreadyExists => continue, diff --git a/lib/std/os.zig b/lib/std/os.zig index cfcab92bd4f1..6e614b800631 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1520,6 +1520,14 @@ pub fn getcwd(out_buffer: []u8) GetCwdError![]u8 { } } +/// Use with `symlink` to specify whether the symlink will point to a file +/// or a directory. This value is ignored on all hosts except Windows where +/// creating symlinks to different resource types, requires different flags. +/// By default, symlink is assumed to point to a file. +pub const SymlinkFlags = struct{ + is_directory: bool = false, +}; + pub const SymLinkError = error{ /// In WASI, this error may occur when the file descriptor does /// not hold the required rights to create a new symbolic link relative to it. @@ -1541,39 +1549,34 @@ pub const SymLinkError = error{ /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. -/// On Windows, it is only legal to create a symbolic link to an existing resource. Furthermore, -/// 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 instead, see `symlinkW` for more information how to -/// do that. /// If `sym_link_path` exists, it will not be overwritten. /// See also `symlinkC` and `symlinkW`. -pub fn symlink(target_path: []const u8, sym_link_path: []const u8) SymLinkError!void { +pub fn symlink(target_path: []const u8, sym_link_path: []const u8, flags: SymlinkFlags) SymLinkError!void { if (builtin.os.tag == .wasi) { @compileError("symlink is not supported in WASI; use symlinkat instead"); } if (builtin.os.tag == .windows) { const target_path_w = try windows.sliceToPrefixedFileW(target_path); const sym_link_path_w = try windows.sliceToPrefixedFileW(sym_link_path); - return symlinkW(target_path_w.span().ptr, sym_link_path_w.span().ptr); + return symlinkW(target_path_w.span().ptr, sym_link_path_w.span().ptr, flags); } const target_path_c = try toPosixPath(target_path); const sym_link_path_c = try toPosixPath(sym_link_path); - return symlinkZ(&target_path_c, &sym_link_path_c); + return symlinkZ(&target_path_c, &sym_link_path_c, flags); } pub const symlinkC = @compileError("deprecated: renamed to symlinkZ"); /// Windows-only. Same as `symlink` except the parameters are null-terminated, WTF16 encoded. /// 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, use `std.os.windows.CreateSymbolicLinkW` directly -/// specifying as flags `std.os.windows.CreateSymbolicLinkFlags.Directory`. -pub fn symlinkW(target_path: [*:0]const u16, sym_link_path: [*:0]const u16) SymLinkError!void { - return windows.CreateSymbolicLinkW(sym_link_path, target_path, false); +/// like to create a symbolic link to a directory, specify this with `SymlinkFlags{ .is_directory = true }`. +pub fn symlinkW(target_path: [*:0]const u16, sym_link_path: [*:0]const u16, flags: SymlinkFlags) SymLinkError!void { + return windows.CreateSymbolicLinkW(sym_link_path, target_path, flags.is_directory); } /// This is the same as `symlink` except the parameters are null-terminated pointers. /// See also `symlink`. -pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8) SymLinkError!void { +pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8, flags: SymlinkFlags) SymLinkError!void { if (builtin.os.tag == .windows) { const target_path_w = try windows.cStrToPrefixedFileW(target_path); const sym_link_path_w = try windows.cStrToPrefixedFileW(sym_link_path); diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 597ec88951ad..5bb1ef38db92 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -48,7 +48,7 @@ test "readlink" { { var cwd = fs.cwd(); try cwd.writeFile("file.txt", "nonsense"); - try os.symlink("file.txt", "symlinked"); + try os.symlink("file.txt", "symlinked", .{}); var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; const given = try os.readlink("symlinked", buffer[0..]); @@ -81,7 +81,7 @@ test "readlink" { std.debug.warn("symlink_path={}\n", .{symlink_path}); // create symbolic link by path - try os.symlink(target_path, symlink_path); + try os.symlink(target_path, symlink_path, .{}); // now, read the link and verify var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; From a8a02dfbfaf4b9bd303712c853e202bd07837371 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 15 Jul 2020 17:40:39 +0200 Subject: [PATCH 13/28] Add smoke test for dir symlinks --- lib/std/os/test.zig | 52 ++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 5bb1ef38db92..98a2cb7fae63 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -44,7 +44,7 @@ test "fstatat" { test "readlink" { if (builtin.os.tag == .wasi) return error.SkipZigTest; - // First, try relative paths + // First, try relative paths in cwd { var cwd = fs.cwd(); try cwd.writeFile("file.txt", "nonsense"); @@ -58,37 +58,41 @@ test "readlink" { try cwd.deleteFile("symlinked"); } - // Next, let's try fully-qualified paths - { - var tmp = tmpDir(.{}); - // defer tmp.cleanup(); - - // create file - try tmp.dir.writeFile("file.txt", "nonsense"); - - // get paths - // TODO: use Dir's realpath function once that exists - 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); - }; - const target_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "file.txt" }); - const symlink_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "symlinked" }); + // Now, let's use tempdir + var tmp = tmpDir(.{}); + // defer tmp.cleanup(); + + // Create some targets + try tmp.dir.writeFile("file.txt", "nonsense"); + try tmp.dir.makeDir("subdir"); + + // 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); + }; + + try testReadlink(&arena.allocator, base_path, "file.txt", "symlink1", false); + try testReadlink(&arena.allocator, base_path, "subdir", "symlink2", true); +} + +fn testReadlink(allocator: *mem.Allocator, base_path: []const u8, target_name: []const u8, symlink_name: []const u8, is_dir: bool) !void { + const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, target_name }); + const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, symlink_name }); std.debug.warn("\ntarget_path={}\n", .{target_path}); std.debug.warn("symlink_path={}\n", .{symlink_path}); - // create symbolic link by path - try os.symlink(target_path, symlink_path, .{}); + // Create symbolic link by path + try os.symlink(target_path, symlink_path, .{ .is_directory = is_dir }); - // now, read the link and verify + // Read the link and verify var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; const given = try os.readlink(symlink_path, buffer[0..]); std.debug.warn("given={}\n", .{given}); expect(mem.eql(u8, target_path, given)); - } } test "readlinkat" { From cc9c5c5b0e7c37f96d7ea4c6bd22118ea72a0265 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 15 Jul 2020 17:55:02 +0200 Subject: [PATCH 14/28] Handle relative/absolute symlinks; add more tests --- lib/std/os.zig | 5 ++++- lib/std/os/test.zig | 41 +++++++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 6e614b800631..5243c1155a6c 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -2437,7 +2437,10 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 { const prefix = [_]u16{ '\\', '?', '?', '\\' }; - const start_index = if (mem.startsWith(u16, path, &prefix)) prefix.len else 0; + var start_index: usize = 0; + if (!is_relative and mem.startsWith(u16, path, &prefix)) { + start_index = prefix.len; + } const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable; return out_buffer[0..out_len]; } diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 98a2cb7fae63..056d06d97542 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -70,29 +70,46 @@ test "readlink" { 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); }; + const allocator = &arena.allocator; - try testReadlink(&arena.allocator, base_path, "file.txt", "symlink1", false); - try testReadlink(&arena.allocator, base_path, "subdir", "symlink2", true); -} + { + const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "file.txt" }); + const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink1" }); + std.debug.warn("\ntarget_path={}\n", .{target_path}); + std.debug.warn("symlink_path={}\n", .{symlink_path}); -fn testReadlink(allocator: *mem.Allocator, base_path: []const u8, target_name: []const u8, symlink_name: []const u8, is_dir: bool) !void { - const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, target_name }); - const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, symlink_name }); + // Create symbolic link by path + try os.symlink(target_path, symlink_path, .{ .is_directory = false }); + try testReadlink(target_path, symlink_path); + } + { + const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "subdir" }); + const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink2" }); std.debug.warn("\ntarget_path={}\n", .{target_path}); std.debug.warn("symlink_path={}\n", .{symlink_path}); // Create symbolic link by path - try os.symlink(target_path, symlink_path, .{ .is_directory = is_dir }); + try os.symlink(target_path, symlink_path, .{ .is_directory = true }); + try testReadlink(target_path, symlink_path); + } - // Read the link and verify - var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; - const given = try os.readlink(symlink_path, buffer[0..]); - std.debug.warn("given={}\n", .{given}); - expect(mem.eql(u8, target_path, given)); + if (builtin.os.tag == .windows) { + try testReadlink("C:\\ProgramData", "C:\\Users\\All Users"); + try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User"); + try testReadlink("C:\\Users", "C:\\Documents and Settings"); + } +} + +fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const given = try os.readlink(symlink_path, buffer[0..]); + std.debug.warn("given={}\n", .{given}); + expect(mem.eql(u8, target_path, given)); } test "readlinkat" { From 3ab5e6b1a97160ddadb90d627ae127a62c3cbd96 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 15 Jul 2020 18:15:20 +0200 Subject: [PATCH 15/28] Ensure we use Win32 prefix in Win32 calls --- lib/std/os.zig | 12 +++---- lib/std/os/test.zig | 5 --- lib/std/os/windows.zig | 71 +++++++++++++++++++++++++++++++----------- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 5243c1155a6c..e638e9f2e15b 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1556,8 +1556,8 @@ pub fn symlink(target_path: []const u8, sym_link_path: []const u8, flags: Symlin @compileError("symlink is not supported in WASI; use symlinkat instead"); } if (builtin.os.tag == .windows) { - const target_path_w = try windows.sliceToPrefixedFileW(target_path); - const sym_link_path_w = try windows.sliceToPrefixedFileW(sym_link_path); + const target_path_w = try windows.sliceToWin32PrefixedFileW(target_path); + const sym_link_path_w = try windows.sliceToWin32PrefixedFileW(sym_link_path); return symlinkW(target_path_w.span().ptr, sym_link_path_w.span().ptr, flags); } const target_path_c = try toPosixPath(target_path); @@ -1578,8 +1578,8 @@ pub fn symlinkW(target_path: [*:0]const u16, sym_link_path: [*:0]const u16, flag /// See also `symlink`. pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8, flags: SymlinkFlags) SymLinkError!void { if (builtin.os.tag == .windows) { - const target_path_w = try windows.cStrToPrefixedFileW(target_path); - const sym_link_path_w = try windows.cStrToPrefixedFileW(sym_link_path); + const target_path_w = try windows.cStrToWin32PrefixedFileW(target_path); + const sym_link_path_w = try windows.cStrToWin32PrefixedFileW(sym_link_path); return symlinkW(target_path_w.span().ptr, sym_link_path_w.span().ptr); } switch (errno(system.symlink(target_path, sym_link_path))) { @@ -2382,7 +2382,7 @@ 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) { - const file_path_w = try windows.sliceToPrefixedFileW(file_path); + const file_path_w = try windows.sliceToWin32PrefixedFileW(file_path); return readlinkW(file_path_w.span().ptr, out_buffer); } else { const file_path_c = try toPosixPath(file_path); @@ -2448,7 +2448,7 @@ fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u /// 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.cStrToPrefixedFileW(file_path); + const file_path_w = try windows.cStrToWin32PrefixedFileW(file_path); return readlinkW(file_path_w.span().ptr, out_buffer); } const rc = system.readlink(file_path, out_buffer.ptr, out_buffer.len); diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 056d06d97542..65179e579f91 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -80,8 +80,6 @@ test "readlink" { { const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "file.txt" }); const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink1" }); - std.debug.warn("\ntarget_path={}\n", .{target_path}); - std.debug.warn("symlink_path={}\n", .{symlink_path}); // Create symbolic link by path try os.symlink(target_path, symlink_path, .{ .is_directory = false }); @@ -90,8 +88,6 @@ test "readlink" { { const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "subdir" }); const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink2" }); - std.debug.warn("\ntarget_path={}\n", .{target_path}); - std.debug.warn("symlink_path={}\n", .{symlink_path}); // Create symbolic link by path try os.symlink(target_path, symlink_path, .{ .is_directory = true }); @@ -108,7 +104,6 @@ test "readlink" { fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; const given = try os.readlink(symlink_path, buffer[0..]); - std.debug.warn("given={}\n", .{given}); expect(mem.eql(u8, target_path, given)); } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 67f200d3395f..ab8705048a32 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1263,40 +1263,80 @@ pub const PathSpace = struct { pub fn span(self: PathSpace) [:0]const u16 { return self.data[0..self.len :0]; } + + fn ensureNtStyle(self: *PathSpace) void { + // > File I/O functions in the Windows API convert "/" to "\" as part of + // > converting the name to an NT-style name, except when using the "\\?\" + // > prefix as detailed in the following sections. + // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation + // Because we want the larger maximum path length for absolute paths, we + // convert forward slashes to backward slashes here. + for (self.data[0..self.len]) |*elem| { + if (elem.* == '/') { + elem.* = '\\'; + } + } + self.data[self.len] = 0; + } }; +/// Same as `sliceToPrefixedFileW` but accepts a pointer +/// to a null-terminated path. pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace { return sliceToPrefixedFileW(mem.spanZ(s)); } +/// Same as `sliceToWin32PrefixedFileW` but accepts a pointer +/// to a null-terminated path. +pub fn cStrToWin32PrefixedFileW(s: [*:0]const u8) !PathSpace { + return sliceToWin32PrefixedFileW(mem.spanZ(s)); +} + +/// 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. pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace { // TODO https://github.com/ziglang/zig/issues/2765 var path_space: PathSpace = undefined; - for (s) |byte| { + const prefix_index: usize = if (mem.startsWith(u8, s, "\\??\\")) 4 else 0; + for (s[prefix_index..]) |byte| { switch (byte) { '*', '?', '"', '<', '>', '|' => return error.BadPathName, else => {}, } } - const start_index = if (mem.startsWith(u8, s, "\\?") or !std.fs.path.isAbsolute(s)) 0 else blk: { + const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: { const prefix = [_]u16{ '\\', '?', '?', '\\' }; mem.copy(u16, path_space.data[0..], &prefix); break :blk prefix.len; }; path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s); if (path_space.len > path_space.data.len) return error.NameTooLong; - // > File I/O functions in the Windows API convert "/" to "\" as part of - // > converting the name to an NT-style name, except when using the "\\?\" - // > prefix as detailed in the following sections. - // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation - // Because we want the larger maximum path length for absolute paths, we - // convert forward slashes to backward slashes here. - for (path_space.data[0..path_space.len]) |*elem| { - if (elem.* == '/') { - elem.* = '\\'; + path_space.ensureNtStyle(); + return path_space; +} + +/// Converts the path `s` to WTF16, null-terminated. If the path is absolute, +/// it will get Win32-style extended prefix `\\?\` prepended automatically. For prepending +/// NT-style prefix, see `sliceToPrefixedFileW` instead. +pub fn sliceToWin32PrefixedFileW(s: []const u8) !PathSpace { + // TODO https://github.com/ziglang/zig/issues/2765 + var path_space: PathSpace = undefined; + const prefix_index: usize = if (mem.startsWith(u8, s, "\\\\?\\")) 4 else 0; + for (s[prefix_index..]) |byte| { + switch (byte) { + '*', '?', '"', '<', '>', '|' => return error.BadPathName, + else => {}, } } - path_space.data[path_space.len] = 0; + const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: { + const prefix = [_]u16{ '\\', '\\', '?', '\\' }; + mem.copy(u16, path_space.data[0..], &prefix); + break :blk prefix.len; + }; + path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s); + if (path_space.len > path_space.data.len) return error.NameTooLong; + path_space.ensureNtStyle(); return path_space; } @@ -1313,12 +1353,7 @@ pub fn wToPrefixedFileW(s: []const u16) !PathSpace { path_space.len = start_index + s.len; if (path_space.len > path_space.data.len) return error.NameTooLong; mem.copy(u16, path_space.data[start_index..], s); - for (path_space.data[0..path_space.len]) |*elem| { - if (elem.* == '/') { - elem.* = '\\'; - } - } - path_space.data[path_space.len] = 0; + path_space.ensureNtStyle(); return path_space; } From 22362568cf309a4ad3d7e48f9e8635f7cb67fba7 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 15 Jul 2020 18:55:07 +0200 Subject: [PATCH 16/28] Refactor --- lib/std/os.zig | 15 +++++++++---- lib/std/os/windows.zig | 51 ++++++++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index e638e9f2e15b..8f86488c0eea 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1524,8 +1524,8 @@ pub fn getcwd(out_buffer: []u8) GetCwdError![]u8 { /// or a directory. This value is ignored on all hosts except Windows where /// creating symlinks to different resource types, requires different flags. /// By default, symlink is assumed to point to a file. -pub const SymlinkFlags = struct{ - is_directory: bool = false, +pub const SymlinkFlags = struct { + is_directory: bool = false, }; pub const SymLinkError = error{ @@ -2372,7 +2372,8 @@ pub const ReadLinkError = error{ NotDir, InvalidUtf8, BadPathName, - /// Windows-only. + /// Windows-only. This error may occur if the opened reparse point is + /// of unsupported type. UnsupportedReparsePointType, } || UnexpectedError; @@ -4075,7 +4076,13 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined; const proc_path = std.fmt.bufPrint(procfs_buf[0..], "/proc/self/fd/{}\x00", .{fd}) catch unreachable; - return readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer); + const target = readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer) catch |err| { + switch (err) { + error.UnsupportedReparsePointType => unreachable, // Windows only, + else => |e| return e, + } + }; + return target; } const result_path = std.c.realpath(pathname, out_buffer) orelse switch (std.c._errno().*) { EINVAL => unreachable, diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index ab8705048a32..82aea077c560 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1296,33 +1296,40 @@ pub fn cStrToWin32PrefixedFileW(s: [*:0]const u8) !PathSpace { /// it will get NT-style prefix `\??\` prepended automatically. For prepending /// Win32-style prefix, see `sliceToWin32PrefixedFileW` instead. pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace { - // TODO https://github.com/ziglang/zig/issues/2765 - var path_space: PathSpace = undefined; - const prefix_index: usize = if (mem.startsWith(u8, s, "\\??\\")) 4 else 0; - for (s[prefix_index..]) |byte| { - switch (byte) { - '*', '?', '"', '<', '>', '|' => return error.BadPathName, - else => {}, - } - } - const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: { - const prefix = [_]u16{ '\\', '?', '?', '\\' }; - mem.copy(u16, path_space.data[0..], &prefix); - break :blk prefix.len; - }; - path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s); - if (path_space.len > path_space.data.len) return error.NameTooLong; - path_space.ensureNtStyle(); - return path_space; + return sliceToPrefixedFileWInternal(s, PathPrefix.Nt); } /// Converts the path `s` to WTF16, null-terminated. If the path is absolute, /// it will get Win32-style extended prefix `\\?\` prepended automatically. For prepending /// NT-style prefix, see `sliceToPrefixedFileW` instead. pub fn sliceToWin32PrefixedFileW(s: []const u8) !PathSpace { + return sliceToPrefixedFileWInternal(s, PathPrefix.Win32); +} + +const PathPrefix = enum { + Win32, + Nt, + + fn toUtf8(self: PathPrefix) []const u8 { + return switch (self) { + .Win32 => "\\\\?\\", + .Nt => "\\??\\", + }; + } + + fn toUtf16(self: PathPrefix) []const u16 { + return switch (self) { + .Win32 => &[_]u16{ '\\', '\\', '?', '\\' }, + .Nt => &[_]u16{ '\\', '?', '?', '\\' }, + }; + } +}; + +fn sliceToPrefixedFileWInternal(s: []const u8, prefix: PathPrefix) !PathSpace { // TODO https://github.com/ziglang/zig/issues/2765 var path_space: PathSpace = undefined; - const prefix_index: usize = if (mem.startsWith(u8, s, "\\\\?\\")) 4 else 0; + const prefix_utf8 = prefix.toUtf8(); + const prefix_index: usize = if (mem.startsWith(u8, s, prefix_utf8)) prefix_utf8.len else 0; for (s[prefix_index..]) |byte| { switch (byte) { '*', '?', '"', '<', '>', '|' => return error.BadPathName, @@ -1330,9 +1337,9 @@ pub fn sliceToWin32PrefixedFileW(s: []const u8) !PathSpace { } } const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: { - const prefix = [_]u16{ '\\', '\\', '?', '\\' }; - mem.copy(u16, path_space.data[0..], &prefix); - break :blk prefix.len; + const prefix_utf16 = prefix.toUtf16(); + mem.copy(u16, path_space.data[0..], prefix_utf16); + break :blk prefix_utf16.len; }; path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s); if (path_space.len > path_space.data.len) return error.NameTooLong; From 08e7ac3028da4503d709553a6c12e5751c6ce4fb Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 15 Jul 2020 20:28:28 +0200 Subject: [PATCH 17/28] Fix compilation on other hosts --- lib/std/zig/system.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index af494efbab2d..6949388120a8 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -552,6 +552,9 @@ pub const NativeTargetInfo = struct { error.SystemResources => return error.SystemResources, error.NotDir => return error.GnuLibCVersionUnavailable, error.Unexpected => return error.GnuLibCVersionUnavailable, + error.InvalidUtf8 => unreachable, // Windows only + error.BadPathName => unreachable, // Windows only + error.UnsupportedReparsePointType => unreachable, // Windows only }; return glibcVerFromLinkName(link_name); } From dd366d316d0b3555864145494b28fdb6e090a3f2 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 15 Jul 2020 20:33:21 +0200 Subject: [PATCH 18/28] Fix more compilation errors on other hosts --- lib/std/zig/system.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 6949388120a8..8e4f92e5eec7 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -820,6 +820,9 @@ pub const NativeTargetInfo = struct { &link_buf, ) catch |err| switch (err) { error.NameTooLong => unreachable, + error.InvalidUtf8 => unreachable, // Windows only + error.BadPathName => unreachable, // Windows only + error.UnsupportedReparsePointType => unreachable, // Windows only error.AccessDenied, error.FileNotFound, From fc7d87fef19964273eb65e8cee05eaffa7a9e72f Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Sun, 19 Jul 2020 11:47:00 +0200 Subject: [PATCH 19/28] Move symlink to fs.symlinkAbsolute with SymlinkFlags This way `std.fs.symlinkAbsolute` becomes cross-platform and we can legally include `SymlinkFlags` as an argument that's only used on Windows. Also, now `std.os.symlink` generates a compile error on Windows with a message to instead use `std.os.windows.CreateSymbolicLink`. Finally, this PR also reshuffles the tests between `std.os.test` and `std.fs.test`. --- lib/std/fs.zig | 67 ++++++++++++++++++++++++--- lib/std/fs/test.zig | 44 ++++++++++++++++++ lib/std/os.zig | 31 +++---------- lib/std/os/test.zig | 101 ++++++++++++++--------------------------- lib/std/os/windows.zig | 4 +- 5 files changed, 148 insertions(+), 99 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 6b4c09845c78..0c7f8f06d5e0 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -16,9 +16,6 @@ pub const wasi = @import("fs/wasi.zig"); // TODO audit these APIs with respect to Dir and absolute paths -pub const symLink = os.symlink; -pub const symLinkZ = os.symlinkZ; -pub const symLinkC = @compileError("deprecated: renamed to symlinkZ"); pub const rename = os.rename; pub const renameZ = os.renameZ; pub const renameC = @compileError("deprecated: renamed to renameZ"); @@ -69,7 +66,7 @@ pub const need_async_thread = std.io.is_async and switch (builtin.os.tag) { /// TODO remove the allocator requirement from this API pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: []const u8) !void { - if (symLink(existing_path, new_path, .{})) { + if (symLinkAbsolute(existing_path, new_path, .{})) { return; } else |err| switch (err) { error.PathAlreadyExists => {}, @@ -87,7 +84,7 @@ pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: try crypto.randomBytes(rand_buf[0..]); base64_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf); - if (symLink(existing_path, tmp_path, .{})) { + if (symLinkAbsolute(existing_path, tmp_path, .{})) { return rename(tmp_path, new_path); } else |err| switch (err) { error.PathAlreadyExists => continue, @@ -1692,8 +1689,15 @@ pub fn readLinkAbsolute(pathname: []const u8, buffer: *[MAX_PATH_BYTES]u8) ![]u8 return os.readlink(pathname, buffer); } +/// Windows-only. Same as `readlinkW`, except the path parameter is null-terminated, WTF16 +/// encoded. +pub fn readlinkAbsoluteW(pathname_w: [*:0]const u16, buffer: *[MAX_PATH_BYTES]u8) ![]u8 { + assert(path.isAbsoluteWindowsW(pathname_w)); + return os.readlinkW(pathname_w, buffer); +} + /// Same as `readLink`, except the path parameter is null-terminated. -pub fn readLinkAbsoluteZ(pathname_c: [*]const u8, buffer: *[MAX_PATH_BYTES]u8) ![]u8 { +pub fn readLinkAbsoluteZ(pathname_c: [*:0]const u8, buffer: *[MAX_PATH_BYTES]u8) ![]u8 { assert(path.isAbsoluteZ(pathname_c)); return os.readlinkZ(pathname_c, buffer); } @@ -1701,6 +1705,57 @@ pub fn readLinkAbsoluteZ(pathname_c: [*]const u8, buffer: *[MAX_PATH_BYTES]u8) ! pub const readLink = @compileError("deprecated; use Dir.readLink or readLinkAbsolute"); pub const readLinkC = @compileError("deprecated; use Dir.readLinkZ or readLinkAbsoluteZ"); +/// Use with `symLinkAbsolute` to specify whether the symlink will point to a file +/// or a directory. This value is ignored on all hosts except Windows where +/// creating symlinks to different resource types, requires different flags. +/// By default, `symLinkAbsolute` is assumed to point to a file. +pub const SymlinkFlags = struct { + is_directory: bool = false, +}; + +/// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. +/// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent +/// one; the latter case is known as a dangling link. +/// If `sym_link_path` exists, it will not be overwritten. +/// See also `symLinkAbsoluteZ` and `symLinkAbsoluteW`. +pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: SymlinkFlags) !void { + if (builtin.os.tag == .wasi) { + @compileError("symLink is not supported in WASI"); + } + assert(path.isAbsolute(target_path)); + assert(path.isAbsolute(sym_link_path)); + if (builtin.os.tag == .windows) { + return os.windows.CreateSymbolicLink(target_path, sym_link_path, flags.is_directory); + } + return os.symlink(target_path, sym_link_path); +} + +/// Windows-only. Same as `symLinkAbsolute` except the parameters are null-terminated, WTF16 encoded. +/// 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(target_path_w, sym_link_path_w, flags.is_directory); +} + +/// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. +/// See also `symLinkAbsolute`. +pub fn symLinkAbsoluteZ(target_path_c: [*:0]const u8, sym_link_path_c: [*:0]const u8, flags: SymlinkFlags) !void { + assert(path.isAbsoluteZ(target_path_c)); + assert(path.isAbsoluteZ(sym_link_path_c)); + 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(target_path_w.span().ptr, sym_link_path_w.span().ptr, flags.is_directory); + } + return os.symlinkZ(target_path_c, sym_link_path_c); +} + +pub const symLink = @compileError("deprecated: use symLinkAbsolute"); +pub const symLinkC = @compileError("deprecated: use symLinkAbsoluteC"); + pub const Walker = struct { stack: std.ArrayList(StackItem), name_buffer: std.ArrayList(u8), diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index a3cf2e800268..c20cb2e62fdf 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -10,6 +10,50 @@ const Dir = std.fs.Dir; const File = std.fs.File; const tmpDir = testing.tmpDir; +test "readLinkAbsolute" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + // Create some targets + try tmp.dir.writeFile("file.txt", "nonsense"); + try tmp.dir.makeDir("subdir"); + + // 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); + }; + const allocator = &arena.allocator; + + { + const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "file.txt" }); + const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink1" }); + + // Create symbolic link by path + try fs.symLinkAbsolute(target_path, symlink_path, .{}); + try testReadlinkAbsolute(target_path, symlink_path); + } + { + const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "subdir" }); + const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink2" }); + + // Create symbolic link by path + try fs.symLinkAbsolute(target_path, symlink_path, .{ .is_directory = true }); + try testReadlinkAbsolute(target_path, symlink_path); + } +} + +fn testReadlinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void { + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]); + testing.expect(mem.eql(u8, target_path, given)); +} + test "Dir.Iterator" { var tmp_dir = tmpDir(.{ .iterate = true }); defer tmp_dir.cleanup(); diff --git a/lib/std/os.zig b/lib/std/os.zig index 8f86488c0eea..ebe457fe4d3b 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1520,14 +1520,6 @@ pub fn getcwd(out_buffer: []u8) GetCwdError![]u8 { } } -/// Use with `symlink` to specify whether the symlink will point to a file -/// or a directory. This value is ignored on all hosts except Windows where -/// creating symlinks to different resource types, requires different flags. -/// By default, symlink is assumed to point to a file. -pub const SymlinkFlags = struct { - is_directory: bool = false, -}; - pub const SymLinkError = error{ /// In WASI, this error may occur when the file descriptor does /// not hold the required rights to create a new symbolic link relative to it. @@ -1550,37 +1542,26 @@ pub const SymLinkError = error{ /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. /// If `sym_link_path` exists, it will not be overwritten. -/// See also `symlinkC` and `symlinkW`. -pub fn symlink(target_path: []const u8, sym_link_path: []const u8, flags: SymlinkFlags) SymLinkError!void { +/// See also `symlinkZ. +pub fn symlink(target_path: []const u8, sym_link_path: []const u8) SymLinkError!void { if (builtin.os.tag == .wasi) { @compileError("symlink is not supported in WASI; use symlinkat instead"); } if (builtin.os.tag == .windows) { - const target_path_w = try windows.sliceToWin32PrefixedFileW(target_path); - const sym_link_path_w = try windows.sliceToWin32PrefixedFileW(sym_link_path); - return symlinkW(target_path_w.span().ptr, sym_link_path_w.span().ptr, flags); + @compileError("symlink is not supported on Windows; use std.os.windows.CreateSymbolicLink instead"); } const target_path_c = try toPosixPath(target_path); const sym_link_path_c = try toPosixPath(sym_link_path); - return symlinkZ(&target_path_c, &sym_link_path_c, flags); + return symlinkZ(&target_path_c, &sym_link_path_c); } pub const symlinkC = @compileError("deprecated: renamed to symlinkZ"); -/// Windows-only. Same as `symlink` except the parameters are null-terminated, WTF16 encoded. -/// 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 }`. -pub fn symlinkW(target_path: [*:0]const u16, sym_link_path: [*:0]const u16, flags: SymlinkFlags) SymLinkError!void { - return windows.CreateSymbolicLinkW(sym_link_path, target_path, flags.is_directory); -} - /// This is the same as `symlink` except the parameters are null-terminated pointers. /// See also `symlink`. -pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8, flags: SymlinkFlags) SymLinkError!void { +pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8) SymLinkError!void { if (builtin.os.tag == .windows) { - const target_path_w = try windows.cStrToWin32PrefixedFileW(target_path); - const sym_link_path_w = try windows.cStrToWin32PrefixedFileW(sym_link_path); - return symlinkW(target_path_w.span().ptr, sym_link_path_w.span().ptr); + @compileError("symlink is not supported on Windows; use std.os.windows.CreateSymbolicLink instead"); } switch (errno(system.symlink(target_path, sym_link_path))) { 0 => return, diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 65179e579f91..0d9420d389d9 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -19,6 +19,41 @@ const tmpDir = std.testing.tmpDir; const Dir = std.fs.Dir; const ArenaAllocator = std.heap.ArenaAllocator; +test "symlink with relative paths" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + // First, try relative paths in cwd + var cwd = fs.cwd(); + try cwd.writeFile("file.txt", "nonsense"); + + if (builtin.os.tag == .windows) { + try os.windows.CreateSymbolicLink("file.txt", "symlinked", false); + } else { + try os.symlink("file.txt", "symlinked"); + } + + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const given = try os.readlink("symlinked", buffer[0..]); + expect(mem.eql(u8, "file.txt", given)); + + try cwd.deleteFile("file.txt"); + try cwd.deleteFile("symlinked"); +} + +test "readlink on Windows" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try testReadlink("C:\\ProgramData", "C:\\Users\\All Users"); + try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User"); + try testReadlink("C:\\Users", "C:\\Documents and Settings"); +} + +fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const given = try os.readlink(symlink_path, buffer[0..]); + expect(mem.eql(u8, target_path, given)); +} + test "fstatat" { // enable when `fstat` and `fstatat` are implemented on Windows if (builtin.os.tag == .windows) return error.SkipZigTest; @@ -41,72 +76,6 @@ test "fstatat" { expectEqual(stat, statat); } -test "readlink" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; - - // First, try relative paths in cwd - { - var cwd = fs.cwd(); - try cwd.writeFile("file.txt", "nonsense"); - try os.symlink("file.txt", "symlinked", .{}); - - var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; - const given = try os.readlink("symlinked", buffer[0..]); - expect(mem.eql(u8, "file.txt", given)); - - try cwd.deleteFile("file.txt"); - try cwd.deleteFile("symlinked"); - } - - // Now, let's use tempdir - var tmp = tmpDir(.{}); - // defer tmp.cleanup(); - - // Create some targets - try tmp.dir.writeFile("file.txt", "nonsense"); - try tmp.dir.makeDir("subdir"); - - // 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); - }; - const allocator = &arena.allocator; - - { - const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "file.txt" }); - const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink1" }); - - // Create symbolic link by path - try os.symlink(target_path, symlink_path, .{ .is_directory = false }); - try testReadlink(target_path, symlink_path); - } - { - const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "subdir" }); - const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink2" }); - - // Create symbolic link by path - try os.symlink(target_path, symlink_path, .{ .is_directory = true }); - try testReadlink(target_path, symlink_path); - } - - if (builtin.os.tag == .windows) { - try testReadlink("C:\\ProgramData", "C:\\Users\\All Users"); - try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User"); - try testReadlink("C:\\Users", "C:\\Documents and Settings"); - } -} - -fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { - var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; - const given = try os.readlink(symlink_path, buffer[0..]); - expect(mem.eql(u8, target_path, given)); -} - test "readlinkat" { // enable when `readlinkat` and `symlinkat` are implemented on Windows if (builtin.os.tag == .windows) return error.SkipZigTest; diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 82aea077c560..8576acc38455 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -609,8 +609,8 @@ pub fn CreateSymbolicLink( 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); + const sym_link_path_w = try sliceToWin32PrefixedFileW(sym_link_path); + const target_path_w = try sliceToWin32PrefixedFileW(target_path); return CreateSymbolicLinkW(sym_link_path_w.span().ptr, target_path_w.span().ptr, is_directory); } From 3c8ceb674eb00ac0a70d6a0742234ce520eccf06 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Sun, 19 Jul 2020 12:21:46 +0200 Subject: [PATCH 20/28] Fix Windows build --- lib/std/fs.zig | 8 ++++---- lib/std/os/test.zig | 2 +- lib/std/os/windows.zig | 10 +++++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 0c7f8f06d5e0..3cdb163f3847 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1720,12 +1720,12 @@ pub const SymlinkFlags = struct { /// See also `symLinkAbsoluteZ` and `symLinkAbsoluteW`. pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: SymlinkFlags) !void { if (builtin.os.tag == .wasi) { - @compileError("symLink is not supported in WASI"); + @compileError("symLinkAbsolute is not supported in WASI"); } assert(path.isAbsolute(target_path)); assert(path.isAbsolute(sym_link_path)); if (builtin.os.tag == .windows) { - return os.windows.CreateSymbolicLink(target_path, sym_link_path, flags.is_directory); + return os.windows.CreateSymbolicLink(sym_link_path, target_path, flags.is_directory); } return os.symlink(target_path, sym_link_path); } @@ -1737,7 +1737,7 @@ pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags 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(target_path_w, sym_link_path_w, flags.is_directory); + return os.windows.CreateSymbolicLinkW(sym_link_path_w, target_path_w, flags.is_directory); } /// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. @@ -1748,7 +1748,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(target_path_w.span().ptr, sym_link_path_w.span().ptr, flags.is_directory); + return os.windows.CreateSymbolicLinkW(sym_link_path_w.span().ptr, target_path_w.span().ptr, flags.is_directory); } return os.symlinkZ(target_path_c, sym_link_path_c); } diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 0d9420d389d9..c95af79e026e 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -27,7 +27,7 @@ test "symlink with relative paths" { try cwd.writeFile("file.txt", "nonsense"); if (builtin.os.tag == .windows) { - try os.windows.CreateSymbolicLink("file.txt", "symlinked", false); + try os.windows.CreateSymbolicLink("symlinked", "file.txt", false); } else { try os.symlink("file.txt", "symlinked"); } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 8576acc38455..7835742e39db 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -602,7 +602,15 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 { return buffer[0..end_index]; } -pub const CreateSymbolicLinkError = error{ AccessDenied, PathAlreadyExists, FileNotFound, Unexpected }; +pub const CreateSymbolicLinkError = error{ + AccessDenied, + PathAlreadyExists, + FileNotFound, + NameTooLong, + InvalidUtf8, + BadPathName, + Unexpected +}; pub fn CreateSymbolicLink( sym_link_path: []const u8, From e0b77a6b77614538d18b9f0b9e3e7e434ccee4ff Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Sun, 19 Jul 2020 22:25:00 +0200 Subject: [PATCH 21/28] Ensure Dir.deleteTree does not dereference symlinks Otherwise, the behaviour can lead to unexpected results, resulting in removing an entire tree that's not necessarily under the root. Furthermore, this change is needed if are to properly handle dir symlinks on Windows. Without explicitly requiring that a directory or file is opened with `FILE_OPEN_REPARSE_POINT`, Windows automatically dereferences all symlinks along the way. This commit adds another option to `OpenDirOptions`, namely `.no_follow`, which defaults to `false` and can be used to specifically open a directory symlink on Windows or call `openat` with `O_NOFOLLOW` flag in POSIX. --- lib/std/fs.zig | 27 ++++++++++++++++++--------- lib/std/fs/test.zig | 6 +++--- lib/std/os.zig | 4 +++- lib/std/os/windows.zig | 10 +--------- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 3cdb163f3847..a22b15b0fb9a 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -951,6 +951,9 @@ pub const Dir = struct { /// `true` means the opened directory can be scanned for the files and sub-directories /// of the result. It means the `iterate` function can be called. iterate: bool = false, + + /// `true` means it won't dereference the symlink. + no_follow: bool = false, }; /// Opens a directory at the given path. The directory is a system resource that remains @@ -994,10 +997,11 @@ pub const Dir = struct { w.RIGHT_PATH_REMOVE_DIRECTORY | w.RIGHT_PATH_UNLINK_FILE; } + const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW; // TODO do we really need all the rights here? const inheriting: w.rights_t = w.RIGHT_ALL ^ w.RIGHT_SOCK_SHUTDOWN; - const result = os.openatWasi(self.fd, sub_path, w.O_DIRECTORY, 0x0, base, inheriting); + const result = os.openatWasi(self.fd, sub_path, w.O_DIRECTORY, symlink_flags, base, inheriting); const fd = result catch |err| switch (err) { error.FileTooBig => unreachable, // can't happen for directories error.IsDir => unreachable, // we're providing O_DIRECTORY @@ -1014,11 +1018,13 @@ pub const Dir = struct { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.cStrToPrefixedFileW(sub_path_c); return self.openDirW(sub_path_w.span().ptr, args); - } else if (!args.iterate) { + } + const symlink_flags: u32 = if (args.no_follow) os.O_NOFOLLOW else 0x0; + if (!args.iterate) { const O_PATH = if (@hasDecl(os, "O_PATH")) os.O_PATH else 0; - return self.openDirFlagsZ(sub_path_c, os.O_DIRECTORY | os.O_RDONLY | os.O_CLOEXEC | O_PATH); + return self.openDirFlagsZ(sub_path_c, os.O_DIRECTORY | os.O_RDONLY | os.O_CLOEXEC | O_PATH | symlink_flags); } else { - return self.openDirFlagsZ(sub_path_c, os.O_DIRECTORY | os.O_RDONLY | os.O_CLOEXEC); + return self.openDirFlagsZ(sub_path_c, os.O_DIRECTORY | os.O_RDONLY | os.O_CLOEXEC | symlink_flags); } } @@ -1030,7 +1036,7 @@ 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 (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; - return self.openDirAccessMaskW(sub_path_w, flags); + return self.openDirAccessMaskW(sub_path_w, flags, args.no_follow); } /// `flags` must contain `os.O_DIRECTORY`. @@ -1050,7 +1056,7 @@ pub const Dir = struct { return Dir{ .fd = fd }; } - fn openDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32) OpenError!Dir { + fn openDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, no_follow: bool) OpenError!Dir { const w = os.windows; var result = Dir{ @@ -1080,6 +1086,7 @@ pub const Dir = struct { // implement this: https://git.midipix.org/ntapi/tree/src/fs/ntapi_tt_open_physical_parent_directory.c @panic("TODO opening '..' with a relative directory handle is not yet implemented on Windows"); } + const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; var io: w.IO_STATUS_BLOCK = undefined; const rc = w.ntdll.NtCreateFile( &result.fd, @@ -1090,7 +1097,7 @@ pub const Dir = struct { 0, w.FILE_SHARE_READ | w.FILE_SHARE_WRITE, w.FILE_OPEN, - w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, null, 0, ); @@ -1277,6 +1284,7 @@ pub const Dir = struct { pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { start_over: while (true) { var got_access_denied = false; + // First, try deleting the item as a file. This way we don't follow sym links. if (self.deleteFile(sub_path)) { return; @@ -1297,7 +1305,8 @@ pub const Dir = struct { error.Unexpected, => |e| return e, } - var dir = self.openDir(sub_path, .{ .iterate = true }) catch |err| switch (err) { + + var dir = self.openDir(sub_path, .{ .iterate = true, .no_follow = true }) catch |err| switch (err) { error.NotDir => { if (got_access_denied) { return error.AccessDenied; @@ -1364,7 +1373,7 @@ pub const Dir = struct { => |e| return e, } - const new_dir = dir.openDir(entry.name, .{ .iterate = true }) catch |err| switch (err) { + const new_dir = dir.openDir(entry.name, .{ .iterate = true, .no_follow = true }) catch |err| switch (err) { error.NotDir => { if (got_access_denied) { return error.AccessDenied; diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index c20cb2e62fdf..4fecf3c9c539 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -36,7 +36,7 @@ test "readLinkAbsolute" { // Create symbolic link by path try fs.symLinkAbsolute(target_path, symlink_path, .{}); - try testReadlinkAbsolute(target_path, symlink_path); + try testReadLinkAbsolute(target_path, symlink_path); } { const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "subdir" }); @@ -44,11 +44,11 @@ test "readLinkAbsolute" { // Create symbolic link by path try fs.symLinkAbsolute(target_path, symlink_path, .{ .is_directory = true }); - try testReadlinkAbsolute(target_path, symlink_path); + try testReadLinkAbsolute(target_path, symlink_path); } } -fn testReadlinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void { +fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void { var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]); testing.expect(mem.eql(u8, target_path, given)); diff --git a/lib/std/os.zig b/lib/std/os.zig index ebe457fe4d3b..86a5a7ad0834 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1815,7 +1815,7 @@ pub fn unlinkatW(dirfd: fd_t, sub_path_w: [*:0]const u16, flags: u32) UnlinkatEr 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) + @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? @@ -2390,9 +2390,11 @@ pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 else => |e| return e, } }; + defer w.CloseHandle(handle); var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; _ = try w.DeviceIoControl(handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); + const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, @alignCast(@alignOf(w.REPARSE_DATA_BUFFER), &reparse_buf[0])); switch (reparse_struct.ReparseTag) { w.IO_REPARSE_TAG_SYMLINK => { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 7835742e39db..3895e5d80a78 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -602,15 +602,7 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 { return buffer[0..end_index]; } -pub const CreateSymbolicLinkError = error{ - AccessDenied, - PathAlreadyExists, - FileNotFound, - NameTooLong, - InvalidUtf8, - BadPathName, - Unexpected -}; +pub const CreateSymbolicLinkError = error{ AccessDenied, PathAlreadyExists, FileNotFound, NameTooLong, InvalidUtf8, BadPathName, Unexpected }; pub fn CreateSymbolicLink( sym_link_path: []const u8, From 2c9c13f624a48e6a22ae84b236eb131c24ba2eb4 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Sun, 19 Jul 2020 23:18:20 +0200 Subject: [PATCH 22/28] Add various build fixes Fix WASI build, fix atomicSymlink by using `cwd().symLink`, add `Dir.symLink` on supported targets. --- lib/std/fs.zig | 77 ++++++++++++++++++++++++++++++++++++++++---------- lib/std/os.zig | 4 +-- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index a22b15b0fb9a..6e0a9defd7b1 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -66,7 +66,15 @@ pub const need_async_thread = std.io.is_async and switch (builtin.os.tag) { /// TODO remove the allocator requirement from this API pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: []const u8) !void { - if (symLinkAbsolute(existing_path, new_path, .{})) { + const res = blk: { + // TODO this is just a temporary until Dir.symLink is implemented on Windows + if (builtin.os.tag == .windows) { + break :blk os.windows.CreateSymbolicLink(new_path, existing_path, false); + } else { + break :blk cwd().symLink(existing_path, new_path, .{}); + } + }; + if (res) { return; } else |err| switch (err) { error.PathAlreadyExists => {}, @@ -84,7 +92,15 @@ pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: try crypto.randomBytes(rand_buf[0..]); base64_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf); - if (symLinkAbsolute(existing_path, tmp_path, .{})) { + const res2 = blk: { + // TODO this is just a temporary until Dir.symLink is implemented on Windows + if (builtin.os.tag == .windows) { + break :blk os.windows.CreateSymbolicLink(tmp_path, existing_path, false); + } else { + break :blk cwd().symLink(existing_path, new_path, .{}); + } + }; + if (res2) { return rename(tmp_path, new_path); } else |err| switch (err) { error.PathAlreadyExists => continue, @@ -669,7 +685,7 @@ pub const Dir = struct { w.RIGHT_FD_FILESTAT_SET_TIMES | w.RIGHT_FD_FILESTAT_SET_SIZE; } - const fd = try os.openatWasi(self.fd, sub_path, 0x0, fdflags, base, 0x0); + const fd = try os.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0); return File{ .handle = fd }; } @@ -789,7 +805,7 @@ pub const Dir = struct { if (flags.exclusive) { oflags |= w.O_EXCL; } - const fd = try os.openatWasi(self.fd, sub_path, oflags, 0x0, base, 0x0); + const fd = try os.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0); return File{ .handle = fd }; } @@ -952,7 +968,7 @@ pub const Dir = struct { /// of the result. It means the `iterate` function can be called. iterate: bool = false, - /// `true` means it won't dereference the symlink. + /// `true` means it won't dereference the symlinks. no_follow: bool = false, }; @@ -1001,7 +1017,7 @@ pub const Dir = struct { // TODO do we really need all the rights here? const inheriting: w.rights_t = w.RIGHT_ALL ^ w.RIGHT_SOCK_SHUTDOWN; - const result = os.openatWasi(self.fd, sub_path, w.O_DIRECTORY, symlink_flags, base, inheriting); + const result = os.openatWasi(self.fd, sub_path, symlink_flags, w.O_DIRECTORY, 0x0, base, inheriting); const fd = result catch |err| switch (err) { error.FileTooBig => unreachable, // can't happen for directories error.IsDir => unreachable, // we're providing O_DIRECTORY @@ -1211,6 +1227,38 @@ pub const Dir = struct { }; } + /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. + /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent + /// one; the latter case is known as a dangling link. + /// If `sym_link_path` exists, it will not be overwritten. + /// TODO add Windows support + pub fn symLink( + self: Dir, + target_path: []const u8, + sym_link_path: []const u8, + flags: SymLinkFlags, + ) !void { + if (builtin.os.tag == .wasi) { + return self.symLinkWasi(target_path, sym_link_path); + } + if (builtin.os.tag == .windows) { + @compileError("TODO implement Dir.symLink on Windows"); + } + const target_path_c = try os.toPosixPath(target_path); + const sym_link_path_c = try os.toPosixPath(sym_link_path); + return self.symLinkZ(&target_path_c, &sym_link_path_c, flags); + } + + /// WASI-only. Same as `symLink` except targeting WASI. + pub fn symLinkWasi(self: Dir, target_path: []const u8, sym_link_path: []const u8, flags: SymLinkFlags) !void { + return os.symlinkatWasi(target_path, self.fd, sym_link_path); + } + + /// Same as `symLink`, except the pathname parameters are null-terminated. + pub fn symLinkZ(self: Dir, target_path_c: [*:0]const u8, sym_link_path_c: [*:0]const u8, flags: SymLinkFlags) !void { + return os.symlinkatZ(target_path_c, self.fd, sym_link_path_c); + } + /// Read value of a symbolic link. /// The return value is a slice of `buffer`, from index `0`. /// Asserts that the path parameter has no null bytes. @@ -1305,7 +1353,6 @@ pub const Dir = struct { error.Unexpected, => |e| return e, } - var dir = self.openDir(sub_path, .{ .iterate = true, .no_follow = true }) catch |err| switch (err) { error.NotDir => { if (got_access_denied) { @@ -1718,7 +1765,7 @@ pub const readLinkC = @compileError("deprecated; use Dir.readLinkZ or readLinkAb /// or a directory. This value is ignored on all hosts except Windows where /// creating symlinks to different resource types, requires different flags. /// By default, `symLinkAbsolute` is assumed to point to a file. -pub const SymlinkFlags = struct { +pub const SymLinkFlags = struct { is_directory: bool = false, }; @@ -1727,9 +1774,9 @@ pub const SymlinkFlags = struct { /// one; the latter case is known as a dangling link. /// If `sym_link_path` exists, it will not be overwritten. /// See also `symLinkAbsoluteZ` and `symLinkAbsoluteW`. -pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: SymlinkFlags) !void { +pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: SymLinkFlags) !void { if (builtin.os.tag == .wasi) { - @compileError("symLinkAbsolute is not supported in WASI"); + @compileError("symLinkAbsolute is not supported in WASI; use Dir.symLinkWasi instead"); } assert(path.isAbsolute(target_path)); assert(path.isAbsolute(sym_link_path)); @@ -1741,9 +1788,9 @@ pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags /// Windows-only. Same as `symLinkAbsolute` except the parameters are null-terminated, WTF16 encoded. /// 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 }`. +/// 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 { +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(sym_link_path_w, target_path_w, flags.is_directory); @@ -1751,7 +1798,7 @@ pub fn symLinkAbsoluteW(target_path_w: [*:0]const u16, sym_link_path_w: [*:0]con /// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. /// See also `symLinkAbsolute`. -pub fn symLinkAbsoluteZ(target_path_c: [*:0]const u8, sym_link_path_c: [*:0]const u8, flags: SymlinkFlags) !void { +pub fn symLinkAbsoluteZ(target_path_c: [*:0]const u8, sym_link_path_c: [*:0]const u8, flags: SymLinkFlags) !void { assert(path.isAbsoluteZ(target_path_c)); assert(path.isAbsoluteZ(sym_link_path_c)); if (builtin.os.tag == .windows) { @@ -1762,8 +1809,8 @@ pub fn symLinkAbsoluteZ(target_path_c: [*:0]const u8, sym_link_path_c: [*:0]cons return os.symlinkZ(target_path_c, sym_link_path_c); } -pub const symLink = @compileError("deprecated: use symLinkAbsolute"); -pub const symLinkC = @compileError("deprecated: use symLinkAbsoluteC"); +pub const symLink = @compileError("deprecated: use Dir.symLink or symLinkAbsolute"); +pub const symLinkC = @compileError("deprecated: use Dir.symLinkZ or symLinkAbsoluteZ"); pub const Walker = struct { stack: std.ArrayList(StackItem), diff --git a/lib/std/os.zig b/lib/std/os.zig index 86a5a7ad0834..36f7669f666e 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1109,10 +1109,10 @@ pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) Ope } /// Open and possibly create a file in WASI. -pub fn openatWasi(dir_fd: fd_t, file_path: []const u8, oflags: oflags_t, fdflags: fdflags_t, base: rights_t, inheriting: rights_t) OpenError!fd_t { +pub fn openatWasi(dir_fd: fd_t, file_path: []const u8, lookup_flags: lookupflags_t, oflags: oflags_t, fdflags: fdflags_t, base: rights_t, inheriting: rights_t) OpenError!fd_t { while (true) { var fd: fd_t = undefined; - switch (wasi.path_open(dir_fd, 0x0, file_path.ptr, file_path.len, oflags, base, inheriting, fdflags, &fd)) { + switch (wasi.path_open(dir_fd, lookup_flags, file_path.ptr, file_path.len, oflags, base, inheriting, fdflags, &fd)) { wasi.ESUCCESS => return fd, wasi.EINTR => continue, From c53bcd027f4522753d2f27c80e69c36980f3f754 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 21 Jul 2020 00:08:48 +0200 Subject: [PATCH 23/28] Start drafting CreateSymbolicLink using ntdll syscalls --- lib/std/os/windows.zig | 46 +++++++++++++++++++++++++++++++++++++ lib/std/os/windows/bits.zig | 1 + 2 files changed, 47 insertions(+) diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 3895e5d80a78..fcfe45e246c7 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -604,6 +604,52 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 { pub const CreateSymbolicLinkError = error{ AccessDenied, PathAlreadyExists, FileNotFound, NameTooLong, InvalidUtf8, BadPathName, Unexpected }; +pub fn NtCreateSymbolicLinkW(dir: ?HANDLE, sym_link_path: []const u16, target_path: []const u16) CreateSymbolicLinkError!void { + const SYMLINK_DATA = extern struct { + ReparseTag: ULONG, + ReparseDataLength: USHORT, + Reserved: USHORT, + SubstituteNameOffset: USHORT, + SubstituteNameLength: USHORT, + PrintNameOffset: USHORT, + PrintNameLength: USHORT, + Flags: ULONG, + }; + + const 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.IsDir => unreachable, // TODO + else => |e| unreachable, + }; + defer CloseHandle(symlink_handle); + + // prepare reparse data buffer + var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; + const buf_len = @sizeOf(SYMLINK_DATA) + target_path.len * 4; + const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2; + const symlink_data = SYMLINK_DATA{ + .ReparseTag = IO_REPARSE_TAG_SYMLINK, + .ReparseDataLength = @intCast(u16, buf_len - header_len), + .Reserved = 0, + .SubstituteNameOffset = @intCast(u16, target_path.len * 2), + .SubstituteNameLength = @intCast(u16, target_path.len * 2), + .PrintNameOffset = 0, + .PrintNameLength = @intCast(u16, target_path.len * 2), + .Flags = if (dir) |_| SYMLINK_FLAG_RELATIVE else 0, + }; + + std.mem.copy(u8, buffer[0..], std.mem.asBytes(&symlink_data)); + @memcpy(buffer[@sizeOf(SYMLINK_DATA)..], @ptrCast([*]const u8, target_path), target_path.len * 2); + const paths_start = @sizeOf(SYMLINK_DATA) + target_path.len * 2; + @memcpy(buffer[paths_start..].ptr, @ptrCast([*]const u8, target_path), target_path.len * 2); + // TODO replace with NtDeviceIoControl + _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null, null); +} + pub fn CreateSymbolicLink( sym_link_path: []const u8, target_path: []const u8, diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index 7295b6a404d3..9f50570e3e46 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -1565,6 +1565,7 @@ pub const MOUNT_POINT_REPARSE_BUFFER = extern struct { PathBuffer: [1]WCHAR, }; pub const MAXIMUM_REPARSE_DATA_BUFFER_SIZE: ULONG = 16 * 1024; +pub const FSCTL_SET_REPARSE_POINT: DWORD = 0x900a4; pub const FSCTL_GET_REPARSE_POINT: DWORD = 0x900a8; pub const IO_REPARSE_TAG_SYMLINK: ULONG = 0xa000000c; pub const IO_REPARSE_TAG_MOUNT_POINT: ULONG = 0xa0000003; From 99f0e64fa0037ae0b8894ec326f854531ef3bfe6 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 21 Jul 2020 08:44:26 +0200 Subject: [PATCH 24/28] Draft out dir symlinks branch --- lib/std/os/windows.zig | 72 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index fcfe45e246c7..f3f5eca450d1 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -604,7 +604,7 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 { pub const CreateSymbolicLinkError = error{ AccessDenied, PathAlreadyExists, FileNotFound, NameTooLong, InvalidUtf8, BadPathName, Unexpected }; -pub fn NtCreateSymbolicLinkW(dir: ?HANDLE, sym_link_path: []const u16, target_path: []const u16) CreateSymbolicLinkError!void { +pub fn NtCreateSymbolicLinkW(dir: ?HANDLE, sym_link_path: [:0]const u16, target_path: [:0]const u16, is_directory: bool) CreateSymbolicLinkError!void { const SYMLINK_DATA = extern struct { ReparseTag: ULONG, ReparseDataLength: USHORT, @@ -616,15 +616,67 @@ pub fn NtCreateSymbolicLinkW(dir: ?HANDLE, sym_link_path: []const u16, target_pa Flags: ULONG, }; - const 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.IsDir => unreachable, // TODO - else => |e| unreachable, - }; + 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) { + else => |e| unreachable, + }; + } defer CloseHandle(symlink_handle); // prepare reparse data buffer From 4887350bf420db3193569e8adb30937e6379db0c Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 21 Jul 2020 09:26:01 +0200 Subject: [PATCH 25/28] Finish drafting CreateSymolicLink using NT calls --- lib/std/fs.zig | 70 ++++++++++++++++++++--------------- lib/std/os/test.zig | 2 +- lib/std/os/windows.zig | 84 +++++++++++++++++------------------------- 3 files changed, 75 insertions(+), 81 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 6e0a9defd7b1..830a4ac7cdad 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -66,15 +66,7 @@ pub const need_async_thread = std.io.is_async and switch (builtin.os.tag) { /// TODO remove the allocator requirement from this API pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: []const u8) !void { - const res = blk: { - // TODO this is just a temporary until Dir.symLink is implemented on Windows - if (builtin.os.tag == .windows) { - break :blk os.windows.CreateSymbolicLink(new_path, existing_path, false); - } else { - break :blk cwd().symLink(existing_path, new_path, .{}); - } - }; - if (res) { + if (cwd().symLink(existing_path, new_path, .{})) { return; } else |err| switch (err) { error.PathAlreadyExists => {}, @@ -92,15 +84,7 @@ pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: try crypto.randomBytes(rand_buf[0..]); base64_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf); - const res2 = blk: { - // TODO this is just a temporary until Dir.symLink is implemented on Windows - if (builtin.os.tag == .windows) { - break :blk os.windows.CreateSymbolicLink(tmp_path, existing_path, false); - } else { - break :blk cwd().symLink(existing_path, new_path, .{}); - } - }; - if (res2) { + if (cwd().symLink(existing_path, new_path, .{})) { return rename(tmp_path, new_path); } else |err| switch (err) { error.PathAlreadyExists => continue, @@ -1239,10 +1223,12 @@ pub const Dir = struct { flags: SymLinkFlags, ) !void { if (builtin.os.tag == .wasi) { - return self.symLinkWasi(target_path, sym_link_path); + return self.symLinkWasi(target_path, sym_link_path, flags); } if (builtin.os.tag == .windows) { - @compileError("TODO implement Dir.symLink on Windows"); + const target_path_w = try os.windows.sliceToPrefixedFileW(target_path); + const sym_link_path_w = try os.windows.sliceToPrefixedFileW(sym_link_path); + return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); } const target_path_c = try os.toPosixPath(target_path); const sym_link_path_c = try os.toPosixPath(sym_link_path); @@ -1250,15 +1236,41 @@ pub const Dir = struct { } /// WASI-only. Same as `symLink` except targeting WASI. - pub fn symLinkWasi(self: Dir, target_path: []const u8, sym_link_path: []const u8, flags: SymLinkFlags) !void { + pub fn symLinkWasi( + self: Dir, + target_path: []const u8, + sym_link_path: []const u8, + flags: SymLinkFlags, + ) !void { return os.symlinkatWasi(target_path, self.fd, sym_link_path); } /// Same as `symLink`, except the pathname parameters are null-terminated. - pub fn symLinkZ(self: Dir, target_path_c: [*:0]const u8, sym_link_path_c: [*:0]const u8, flags: SymLinkFlags) !void { + pub fn symLinkZ( + self: Dir, + target_path_c: [*:0]const u8, + sym_link_path_c: [*:0]const u8, + flags: SymLinkFlags, + ) !void { + if (builtin.os.tag == .windows) { + const target_path_w = try os.windows.cStrToPrefixedFileW(target_path_c); + const sym_link_path_w = try os.windows.cStrToPrefixedFileW(sym_link_path_c); + return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); + } return os.symlinkatZ(target_path_c, self.fd, sym_link_path_c); } + /// Windows-only. Same as `symLink` except the pathname parameters + /// are null-terminated, WTF16 encoded. + pub fn symLinkW( + self: Dir, + target_path_w: [:0]const u16, + sym_link_path_w: [:0]const u16, + flags: SymLinkFlags, + ) !void { + return os.windows.CreateSymbolicLinkW(self.fd, sym_link_path_w, target_path_w, flags.is_directory); + } + /// Read value of a symbolic link. /// The return value is a slice of `buffer`, from index `0`. /// Asserts that the path parameter has no null bytes. @@ -1761,10 +1773,10 @@ pub fn readLinkAbsoluteZ(pathname_c: [*:0]const u8, buffer: *[MAX_PATH_BYTES]u8) pub const readLink = @compileError("deprecated; use Dir.readLink or readLinkAbsolute"); pub const readLinkC = @compileError("deprecated; use Dir.readLinkZ or readLinkAbsoluteZ"); -/// Use with `symLinkAbsolute` to specify whether the symlink will point to a file -/// or a directory. This value is ignored on all hosts except Windows where -/// creating symlinks to different resource types, requires different flags. -/// By default, `symLinkAbsolute` is assumed to point to a file. +/// Use with `Dir.symLink` and `symLinkAbsolute` to specify whether the symlink +/// will point to a file or a directory. This value is ignored on all hosts +/// except Windows where creating symlinks to different resource types, requires +/// different flags. By default, `symLinkAbsolute` is assumed to point to a file. pub const SymLinkFlags = struct { is_directory: bool = false, }; @@ -1781,7 +1793,7 @@ 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(sym_link_path, target_path, flags.is_directory); + return os.windows.CreateSymbolicLink(null, sym_link_path, target_path, flags.is_directory); } return os.symlink(target_path, sym_link_path); } @@ -1790,10 +1802,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 { +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(sym_link_path_w, target_path_w, flags.is_directory); + return os.windows.CreateSymbolicLinkW(null, sym_link_path_w, target_path_w, flags.is_directory); } /// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index c95af79e026e..bb0893ab4c1c 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -27,7 +27,7 @@ test "symlink with relative paths" { try cwd.writeFile("file.txt", "nonsense"); if (builtin.os.tag == .windows) { - try os.windows.CreateSymbolicLink("symlinked", "file.txt", false); + try os.windows.CreateSymbolicLink(cwd.fd, "symlinked", "file.txt", false); } else { try os.symlink("file.txt", "symlinked"); } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index f3f5eca450d1..219b07dbc06e 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -602,9 +602,34 @@ pub fn GetCurrentDirectory(buffer: []u8) GetCurrentDirectoryError![]u8 { return buffer[0..end_index]; } -pub const CreateSymbolicLinkError = error{ AccessDenied, PathAlreadyExists, FileNotFound, NameTooLong, InvalidUtf8, BadPathName, Unexpected }; +pub const CreateSymbolicLinkError = error{ + AccessDenied, + 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 NtCreateSymbolicLinkW(dir: ?HANDLE, sym_link_path: [:0]const u16, target_path: [:0]const u16, is_directory: bool) CreateSymbolicLinkError!void { +pub fn CreateSymbolicLinkW( + dir: ?HANDLE, + sym_link_path: [:0]const u16, + target_path: [:0]const u16, + is_directory: bool, +) CreateSymbolicLinkError!void { const SYMLINK_DATA = extern struct { ReparseTag: ULONG, ReparseDataLength: USHORT, @@ -660,7 +685,7 @@ pub fn NtCreateSymbolicLinkW(dir: ?HANDLE, sym_link_path: [:0]const u16, target_ .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, + .NO_MEDIA_IN_DEVICE => return error.NoDevice, .INVALID_PARAMETER => unreachable, .ACCESS_DENIED => return error.AccessDenied, .OBJECT_PATH_SYNTAX_BAD => unreachable, @@ -674,7 +699,11 @@ pub fn NtCreateSymbolicLinkW(dir: ?HANDLE, sym_link_path: [:0]const u16, target_ .creation = FILE_CREATE, .io_mode = .blocking, }) catch |err| switch (err) { - else => |e| unreachable, + error.WouldBlock => unreachable, + error.IsDir => return error.PathAlreadyExists, + error.PipeBusy => unreachable, + error.SharingViolation => return error.AccessDenied, + else => |e| return e, }; } defer CloseHandle(symlink_handle); @@ -702,53 +731,6 @@ pub fn NtCreateSymbolicLinkW(dir: ?HANDLE, sym_link_path: [:0]const u16, target_ _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null, null); } -pub fn CreateSymbolicLink( - sym_link_path: []const u8, - target_path: []const u8, - is_directory: bool, -) CreateSymbolicLinkError!void { - const sym_link_path_w = try sliceToWin32PrefixedFileW(sym_link_path); - const target_path_w = try sliceToWin32PrefixedFileW(target_path); - return CreateSymbolicLinkW(sym_link_path_w.span().ptr, target_path_w.span().ptr, is_directory); -} - -pub fn CreateSymbolicLinkW( - sym_link_path: [*:0]const u16, - target_path: [*:0]const u16, - is_directory: bool, -) CreateSymbolicLinkError!void { - // Previously, until Win 10 Creators Update, creating symbolic links required - // SeCreateSymbolicLink privilege. Currently, this is no longer required if the - // OS is in Developer Mode; however, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE - // must be added to the input flags. - const flags = if (is_directory) SYMBOLIC_LINK_FLAG_DIRECTORY else 0; - if (kernel32.CreateSymbolicLinkW(sym_link_path, target_path, flags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { - switch (kernel32.GetLastError()) { - .INVALID_PARAMETER => { - // If we're on Windows pre Creators Update, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE - // flag is an invalid parameter, in which case repeat without the flag. - if (kernel32.CreateSymbolicLinkW(sym_link_path, target_path, flags) == 0) { - switch (kernel32.GetLastError()) { - .PRIVILEGE_NOT_HELD => return error.AccessDenied, - .FILE_NOT_FOUND => return error.FileNotFound, - .PATH_NOT_FOUND => return error.FileNotFound, - .ACCESS_DENIED => return error.AccessDenied, - .ALREADY_EXISTS => return error.PathAlreadyExists, - else => |err| return unexpectedError(err), - } - } - return; - }, - .PRIVILEGE_NOT_HELD => return error.AccessDenied, - .FILE_NOT_FOUND => return error.FileNotFound, - .PATH_NOT_FOUND => return error.FileNotFound, - .ACCESS_DENIED => return error.AccessDenied, - .ALREADY_EXISTS => return error.PathAlreadyExists, - else => |err| return unexpectedError(err), - } - } -} - pub const DeleteFileError = error{ FileNotFound, AccessDenied, From 3d41d3fb6e875192ab5f859ca4ef95c894df7fe4 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 21 Jul 2020 19:22:22 +0200 Subject: [PATCH 26/28] Draft out ReadLinkW using NT primitives --- lib/std/os.zig | 71 +---------- lib/std/os/windows.zig | 268 +++++++++++++++++++++-------------------- 2 files changed, 144 insertions(+), 195 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 36f7669f666e..71bf34acfe80 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -2364,8 +2364,7 @@ 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) { - const file_path_w = try windows.sliceToWin32PrefixedFileW(file_path); - return readlinkW(file_path_w.span().ptr, out_buffer); + return windows.ReadLink(std.fs.cwd().fd, file_path, out_buffer); } else { const file_path_c = try toPosixPath(file_path); return readlinkZ(&file_path_c, out_buffer); @@ -2377,56 +2376,7 @@ pub const readlinkC = @compileError("deprecated: renamed to readlinkZ"); /// Windows-only. Same as `readlink` except `file_path` is null-terminated, WTF16 encoded. /// See also `readlinkZ`. pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 { - const w = windows; - - const sharing = w.FILE_SHARE_DELETE | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE; - const disposition = w.OPEN_EXISTING; - const flags = w.FILE_FLAG_BACKUP_SEMANTICS | w.FILE_FLAG_OPEN_REPARSE_POINT; - const handle = w.CreateFileW(file_path, 0, sharing, null, disposition, flags, null) catch |err| { - switch (err) { - error.SharingViolation => return error.AccessDenied, - error.PipeBusy => unreachable, - error.PathAlreadyExists => unreachable, - else => |e| return e, - } - }; - defer w.CloseHandle(handle); - - var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; - _ = try w.DeviceIoControl(handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); - - const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, @alignCast(@alignOf(w.REPARSE_DATA_BUFFER), &reparse_buf[0])); - switch (reparse_struct.ReparseTag) { - w.IO_REPARSE_TAG_SYMLINK => { - const buf = @ptrCast(*const w.SYMBOLIC_LINK_REPARSE_BUFFER, @alignCast(@alignOf(w.SYMBOLIC_LINK_REPARSE_BUFFER), &reparse_struct.DataBuffer[0])); - const offset = buf.SubstituteNameOffset >> 1; - const len = buf.SubstituteNameLength >> 1; - const path_buf = @as([*]const u16, &buf.PathBuffer); - const is_relative = buf.Flags & w.SYMLINK_FLAG_RELATIVE != 0; - return parseReadlinkPath(path_buf[offset .. offset + len], is_relative, out_buffer); - }, - w.IO_REPARSE_TAG_MOUNT_POINT => { - const buf = @ptrCast(*const w.MOUNT_POINT_REPARSE_BUFFER, @alignCast(@alignOf(w.MOUNT_POINT_REPARSE_BUFFER), &reparse_struct.DataBuffer[0])); - const offset = buf.SubstituteNameOffset >> 1; - const len = buf.SubstituteNameLength >> 1; - const path_buf = @as([*]const u16, &buf.PathBuffer); - return parseReadlinkPath(path_buf[offset .. offset + len], false, out_buffer); - }, - else => |value| { - std.debug.warn("unsupported symlink type: {}", .{value}); - return error.UnsupportedReparsePointType; - }, - } -} - -fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 { - const prefix = [_]u16{ '\\', '?', '?', '\\' }; - var start_index: usize = 0; - if (!is_relative and mem.startsWith(u16, path, &prefix)) { - start_index = prefix.len; - } - const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable; - return out_buffer[0..out_len]; + return windows.ReadLinkW(std.fs.cwd().fd, file_path, out_buffer); } /// Same as `readlink` except `file_path` is null-terminated. @@ -2459,8 +2409,7 @@ 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) { - const file_path_w = try windows.cStrToPrefixedFileW(file_path); - return readlinkatW(dirfd, file_path.span().ptr, out_buffer); + return windows.ReadLink(dirfd, file_path, out_buffer); } const file_path_c = try toPosixPath(file_path); return readlinkatZ(dirfd, &file_path_c, out_buffer); @@ -2491,19 +2440,7 @@ 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 { - const w = windows; - - const handle = w.OpenReparsePoint(dir, file_path) catch |err| { - switch (err) { - error.SharingViolation => return error.AccessDenied, - error.PathAlreadyExists => unreachable, - error.PipeBusy => unreachable, - error.PathAlreadyExists => unreachable, - error.NoDevice => return error.FileNotFound, - else => |e| return e, - } - }; - @compileError("TODO implement on Windows"); + return windows.ReadLinkW(dirfd, file_path, out_buffer); } /// Same as `readlinkat` except `file_path` is null-terminated. diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 219b07dbc06e..ab549a0ebf73 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -731,6 +731,117 @@ pub fn CreateSymbolicLinkW( _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null, null); } +pub const ReadLinkError = error{ + FileNotFound, + AccessDenied, + 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) { + 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 = null, + .SecurityQualityOfService = null, + }; + var io: IO_STATUS_BLOCK = undefined; + var result_handle: HANDLE = undefined; + const rc = ntdll.NtCreateFile( + &result_handle, + FILE_READ_ATTRIBUTES, + &attr, + &io, + null, + FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ, + FILE_OPEN, + FILE_OPEN_REPARSE_POINT, + 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.FileNotFound, + .INVALID_PARAMETER => unreachable, + .SHARING_VIOLATION => return error.AccessDenied, + .ACCESS_DENIED => return error.AccessDenied, + .PIPE_BUSY => return error.AccessDenied, + .OBJECT_PATH_SYNTAX_BAD => unreachable, + .OBJECT_NAME_COLLISION => unreachable, + .FILE_IS_A_DIRECTORY => unreachable, + else => return unexpectedStatus(rc), + } + defer CloseHandle(result_handle); + + var reparse_buf: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; + _ = try DeviceIoControl(result_handle, FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null); + + const reparse_struct = @ptrCast(*const REPARSE_DATA_BUFFER, @alignCast(@alignOf(REPARSE_DATA_BUFFER), &reparse_buf[0])); + switch (reparse_struct.ReparseTag) { + IO_REPARSE_TAG_SYMLINK => { + const buf = @ptrCast(*const SYMBOLIC_LINK_REPARSE_BUFFER, @alignCast(@alignOf(SYMBOLIC_LINK_REPARSE_BUFFER), &reparse_struct.DataBuffer[0])); + const offset = buf.SubstituteNameOffset >> 1; + const len = buf.SubstituteNameLength >> 1; + const path_buf = @as([*]const u16, &buf.PathBuffer); + const is_relative = buf.Flags & SYMLINK_FLAG_RELATIVE != 0; + return parseReadlinkPath(path_buf[offset .. offset + len], is_relative, out_buffer); + }, + IO_REPARSE_TAG_MOUNT_POINT => { + const buf = @ptrCast(*const MOUNT_POINT_REPARSE_BUFFER, @alignCast(@alignOf(MOUNT_POINT_REPARSE_BUFFER), &reparse_struct.DataBuffer[0])); + const offset = buf.SubstituteNameOffset >> 1; + const len = buf.SubstituteNameLength >> 1; + const path_buf = @as([*]const u16, &buf.PathBuffer); + return parseReadlinkPath(path_buf[offset .. offset + len], false, out_buffer); + }, + else => |value| { + std.debug.warn("unsupported symlink type: {}", .{value}); + return error.UnsupportedReparsePointType; + }, + } +} + +fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 { + const prefix = [_]u16{ '\\', '?', '?', '\\' }; + var start_index: usize = 0; + if (!is_relative and std.mem.startsWith(u16, path, &prefix)) { + start_index = prefix.len; + } + const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable; + return out_buffer[0..out_len]; +} + pub const DeleteFileError = error{ FileNotFound, AccessDenied, @@ -1343,21 +1454,6 @@ pub const PathSpace = struct { pub fn span(self: PathSpace) [:0]const u16 { return self.data[0..self.len :0]; } - - fn ensureNtStyle(self: *PathSpace) void { - // > File I/O functions in the Windows API convert "/" to "\" as part of - // > converting the name to an NT-style name, except when using the "\\?\" - // > prefix as detailed in the following sections. - // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation - // Because we want the larger maximum path length for absolute paths, we - // convert forward slashes to backward slashes here. - for (self.data[0..self.len]) |*elem| { - if (elem.* == '/') { - elem.* = '\\'; - } - } - self.data[self.len] = 0; - } }; /// Same as `sliceToPrefixedFileW` but accepts a pointer @@ -1366,50 +1462,14 @@ pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace { return sliceToPrefixedFileW(mem.spanZ(s)); } -/// Same as `sliceToWin32PrefixedFileW` but accepts a pointer -/// to a null-terminated path. -pub fn cStrToWin32PrefixedFileW(s: [*:0]const u8) !PathSpace { - return sliceToWin32PrefixedFileW(mem.spanZ(s)); -} - /// 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. pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace { - return sliceToPrefixedFileWInternal(s, PathPrefix.Nt); -} - -/// Converts the path `s` to WTF16, null-terminated. If the path is absolute, -/// it will get Win32-style extended prefix `\\?\` prepended automatically. For prepending -/// NT-style prefix, see `sliceToPrefixedFileW` instead. -pub fn sliceToWin32PrefixedFileW(s: []const u8) !PathSpace { - return sliceToPrefixedFileWInternal(s, PathPrefix.Win32); -} - -const PathPrefix = enum { - Win32, - Nt, - - fn toUtf8(self: PathPrefix) []const u8 { - return switch (self) { - .Win32 => "\\\\?\\", - .Nt => "\\??\\", - }; - } - - fn toUtf16(self: PathPrefix) []const u16 { - return switch (self) { - .Win32 => &[_]u16{ '\\', '\\', '?', '\\' }, - .Nt => &[_]u16{ '\\', '?', '?', '\\' }, - }; - } -}; - -fn sliceToPrefixedFileWInternal(s: []const u8, prefix: PathPrefix) !PathSpace { // TODO https://github.com/ziglang/zig/issues/2765 var path_space: PathSpace = undefined; - const prefix_utf8 = prefix.toUtf8(); - const prefix_index: usize = if (mem.startsWith(u8, s, prefix_utf8)) prefix_utf8.len else 0; + const prefix = "\\??\\"; + const prefix_index: usize = if (mem.startsWith(u8, s, prefix)) prefix.len else 0; for (s[prefix_index..]) |byte| { switch (byte) { '*', '?', '"', '<', '>', '|' => return error.BadPathName, @@ -1417,13 +1477,24 @@ fn sliceToPrefixedFileWInternal(s: []const u8, prefix: PathPrefix) !PathSpace { } } const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: { - const prefix_utf16 = prefix.toUtf16(); - mem.copy(u16, path_space.data[0..], prefix_utf16); - break :blk prefix_utf16.len; + const prefix_u16 = [_]u16{ '\\', '?', '?', '\\' }; + mem.copy(u16, path_space.data[0..], prefix_u16[0..]); + break :blk prefix_u16.len; }; path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s); if (path_space.len > path_space.data.len) return error.NameTooLong; - path_space.ensureNtStyle(); + // > File I/O functions in the Windows API convert "/" to "\" as part of + // > converting the name to an NT-style name, except when using the "\\?\" + // > prefix as detailed in the following sections. + // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation + // Because we want the larger maximum path length for absolute paths, we + // convert forward slashes to backward slashes here. + for (path_space.data[0..path_space.len]) |*elem| { + if (elem.* == '/') { + elem.* = '\\'; + } + } + path_space.data[path_space.len] = 0; return path_space; } @@ -1440,7 +1511,18 @@ pub fn wToPrefixedFileW(s: []const u16) !PathSpace { path_space.len = start_index + s.len; if (path_space.len > path_space.data.len) return error.NameTooLong; mem.copy(u16, path_space.data[start_index..], s); - path_space.ensureNtStyle(); + // > File I/O functions in the Windows API convert "/" to "\" as part of + // > converting the name to an NT-style name, except when using the "\\?\" + // > prefix as detailed in the following sections. + // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation + // Because we want the larger maximum path length for absolute paths, we + // convert forward slashes to backward slashes here. + for (path_space.data[0..path_space.len]) |*elem| { + if (elem.* == '/') { + elem.* = '\\'; + } + } + path_space.data[path_space.len] = 0; return path_space; } @@ -1484,73 +1566,3 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError { } return error.Unexpected; } - -pub const OpenReparsePointError = error{ - FileNotFound, - NoDevice, - SharingViolation, - AccessDenied, - PipeBusy, - PathAlreadyExists, - Unexpected, - NameTooLong, -}; - -/// Open file as a reparse point -pub fn OpenReparsePoint( - dir: ?HANDLE, - sub_path_w: [*:0]const u16, -) OpenReparsePointError!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 = null, - .SecurityQualityOfService = null, - }; - var io: IO_STATUS_BLOCK = undefined; - var result_handle: HANDLE = undefined; - const rc = ntdll.NtCreateFile( - &result_handle, - FILE_READ_ATTRIBUTES, - &attr, - &io, - null, - FILE_ATTRIBUTE_NORMAL, - FILE_SHARE_READ, - FILE_OPEN, - FILE_OPEN_REPARSE_POINT, - 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, - .SHARING_VIOLATION => return error.SharingViolation, - .ACCESS_DENIED => return error.AccessDenied, - .PIPE_BUSY => return error.PipeBusy, - .OBJECT_PATH_SYNTAX_BAD => unreachable, - .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - .FILE_IS_A_DIRECTORY => unreachable, - else => return unexpectedStatus(rc), - } -} From 65581b37cb4027d025ae7b0fc643ff0bd9c7f769 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 21 Jul 2020 19:45:20 +0200 Subject: [PATCH 27/28] Enable std.os.symlinkat tests on Windows --- lib/std/os/test.zig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index bb0893ab4c1c..7310562d64dc 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -77,9 +77,6 @@ test "fstatat" { } test "readlinkat" { - // enable when `readlinkat` and `symlinkat` are implemented on Windows - if (builtin.os.tag == .windows) return error.SkipZigTest; - var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -87,7 +84,11 @@ test "readlinkat" { try tmp.dir.writeFile("file.txt", "nonsense"); // create a symbolic link - try os.symlinkat("file.txt", tmp.dir.fd, "link"); + if (builtin.os.tag == .windows) { + try os.windows.CreateSymbolicLink(tmp.dir.fd, "link", "file.txt", false); + } else { + try os.symlinkat("file.txt", tmp.dir.fd, "link"); + } // read the link var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; From aa6fcaf76f00453f160545e760c37d64588f4517 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 21 Jul 2020 20:40:34 +0200 Subject: [PATCH 28/28] Add missing cross-platform Dir.readLink fns --- lib/std/fs.zig | 22 +++++++++++++++++++++- lib/std/fs/test.zig | 26 ++++++++++++++++++++++++++ lib/std/os.zig | 14 ++------------ 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 830a4ac7cdad..9070fd0a985b 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1215,7 +1215,6 @@ pub const Dir = struct { /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. /// If `sym_link_path` exists, it will not be overwritten. - /// TODO add Windows support pub fn symLink( self: Dir, target_path: []const u8, @@ -1275,17 +1274,38 @@ pub const Dir = struct { /// The return value is a slice of `buffer`, from index `0`. /// Asserts that the path parameter has no null bytes. pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 { + if (builtin.os.tag == .wasi) { + return self.readLinkWasi(sub_path, buffer); + } + if (builtin.os.tag == .windows) { + return os.windows.ReadLink(self.fd, sub_path, buffer); + } const sub_path_c = try os.toPosixPath(sub_path); return self.readLinkZ(&sub_path_c, buffer); } pub const readLinkC = @compileError("deprecated: renamed to readLinkZ"); + /// WASI-only. Same as `readLink` except targeting WASI. + pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 { + return os.readlinkatWasi(self.fd, sub_path, buffer); + } + /// Same as `readLink`, except the `pathname` parameter is null-terminated. 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 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); + } + /// On success, caller owns returned buffer. /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. pub fn readFileAlloc(self: Dir, allocator: *mem.Allocator, file_path: []const u8, max_bytes: usize) ![]u8 { diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 4fecf3c9c539..c9f171196776 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -10,6 +10,32 @@ const Dir = std.fs.Dir; const File = std.fs.File; const tmpDir = testing.tmpDir; +test "Dir.readLink" { + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + // Create some targets + try tmp.dir.writeFile("file.txt", "nonsense"); + try tmp.dir.makeDir("subdir"); + + { + // Create symbolic link by path + try tmp.dir.symLink("file.txt", "symlink1", .{}); + try testReadLink(tmp.dir, "file.txt", "symlink1"); + } + { + // Create symbolic link by path + try tmp.dir.symLink("subdir", "symlink2", .{ .is_directory = true }); + try testReadLink(tmp.dir, "subdir", "symlink2"); + } +} + +fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void { + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const given = try dir.readLink(symlink_path, buffer[0..]); + testing.expect(mem.eql(u8, target_path, given)); +} + test "readLinkAbsolute" { if (builtin.os.tag == .wasi) return error.SkipZigTest; diff --git a/lib/std/os.zig b/lib/std/os.zig index 71bf34acfe80..3e05ac41e53c 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1594,9 +1594,7 @@ pub fn symlinkat(target_path: []const u8, newdirfd: fd_t, sym_link_path: []const return symlinkatWasi(target_path, newdirfd, sym_link_path); } if (builtin.os.tag == .windows) { - const target_path_w = try windows.sliceToPrefixedFileW(target_path); - const sym_link_path_w = try windows.sliceToPrefixedFileW(sym_link_path); - return symlinkatW(target_path_w.span().ptr, newdirfd, sym_link_path_w.span().ptr); + @compileError("symlinkat is not supported on Windows; use std.os.windows.CreateSymbolicLink instead"); } const target_path_c = try toPosixPath(target_path); const sym_link_path_c = try toPosixPath(sym_link_path); @@ -1629,19 +1627,11 @@ pub fn symlinkatWasi(target_path: []const u8, newdirfd: fd_t, sym_link_path: []c } } -/// Windows-only. The same as `symlinkat` except the paths are null-terminated, WTF-16 encoded. -/// See also `symlinkat`. -pub fn symlinkatW(target_path: [*:0]const u16, newdirfd: fd_t, sym_link_path: [*:0]const u16) SymLinkError!void { - @compileError("TODO implement on Windows"); -} - /// The same as `symlinkat` except the parameters are null-terminated pointers. /// See also `symlinkat`. pub fn symlinkatZ(target_path: [*:0]const u8, newdirfd: fd_t, sym_link_path: [*:0]const u8) SymLinkError!void { if (builtin.os.tag == .windows) { - const target_path_w = try windows.cStrToPrefixedFileW(target_path); - const sym_link_path_w = try windows.cStrToPrefixedFileW(sym_link_path); - return symlinkatW(target_path_w.span().ptr, newdirfd, sym_link_path.span().ptr); + @compileError("symlinkat is not supported on Windows; use std.os.windows.CreateSymbolicLink instead"); } switch (errno(system.symlinkat(target_path, newdirfd, sym_link_path))) { 0 => return,