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 dfb47208ca9cecb04dd46de155279b9bf7928fa0..983f7f9a90cd8fba2a28fe2311f88aab512b5d5c 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 a6a9e50d53d41564cc9985042b265e243f3f51e4..6182dc5de82646d8161b45f107e2baf20cba24b0 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; -- 2.54.0 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 983f7f9a90cd8fba2a28fe2311f88aab512b5d5c..ab54ba5fe88bef024c484f2ff9d7f6c0d7bbf97d 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 e508f5ae20f3fba20c742f27ba94efeb79204784..b3f0136cf6af874c108dc9583b89fac96e9f4ae7 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 6182dc5de82646d8161b45f107e2baf20cba24b0..998d4a80913583091c59679a3913d83bf216be44 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; -- 2.54.0 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 ab54ba5fe88bef024c484f2ff9d7f6c0d7bbf97d..67b7dfe05b053149a1876a18b235e44f279bf4a1 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 1ed1ef1f54ca39357aefb1f553f18c582fc03c33..5439bc0df9880d627550a28d173025c8d1e97e2d 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 998d4a80913583091c59679a3913d83bf216be44..715a3cce7e63dd9ddfbfad183ea77b1d0f688bca 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 -- 2.54.0 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 67b7dfe05b053149a1876a18b235e44f279bf4a1..eb033babfb16438bb107c769a455b45689d4dcb6 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 b3f0136cf6af874c108dc9583b89fac96e9f4ae7..eb2da384a6bac30a0208d5ef7e973b6b7f85ad60 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 5439bc0df9880d627550a28d173025c8d1e97e2d..049a2a861234def0ad7ccaaf23204c6851adb032 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), } } -- 2.54.0 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 eb033babfb16438bb107c769a455b45689d4dcb6..f9c50adc06335ee3f78a049b8a20d9e78095d44b 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 eb2da384a6bac30a0208d5ef7e973b6b7f85ad60..826be1cc8e6670503091ef69c14573a5569a5023 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 049a2a861234def0ad7ccaaf23204c6851adb032..9fc0a123befa45635eb903c5a61cc9082f2b1063 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 -- 2.54.0 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 f9c50adc06335ee3f78a049b8a20d9e78095d44b..75254b52c84edc52d70228de0c7844868ed61c30 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 9fc0a123befa45635eb903c5a61cc9082f2b1063..0a850a25c42b6553f7aff3f891a56c74c21113c5 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 715a3cce7e63dd9ddfbfad183ea77b1d0f688bca..fef9af9bdaa8f4675ac69d6c8df174e8016ecfe8 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; -- 2.54.0 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 75254b52c84edc52d70228de0c7844868ed61c30..24de1d83f2d03f4eb88bf1657cf3683fba7354a2 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 fef9af9bdaa8f4675ac69d6c8df174e8016ecfe8..66ea5bb512407d6a3d6d29ed28d93aca80de71e8 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 MAXIMUM_REPARSE_DATA_BUFFER_SIZE: usize = 16 * 1024; +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: 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; -- 2.54.0 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 24de1d83f2d03f4eb88bf1657cf3683fba7354a2..e3e44b6a3008c0f861e5f6a31d85496263b1915a 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 826be1cc8e6670503091ef69c14573a5569a5023..a444df3e87f15f6a97f1cf49eae981a611f54bd7 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 0a850a25c42b6553f7aff3f891a56c74c21113c5..d6d1b2db84d60fb47d98d36c723fda6b7322f76d 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 66ea5bb512407d6a3d6d29ed28d93aca80de71e8..8d3043f76ff32eac073318772a4e75b7db17b71b 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; -- 2.54.0 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 | 81 ++++++++++++++++++++----------------- lib/std/os/windows/bits.zig | 4 +- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index e3e44b6a3008c0f861e5f6a31d85496263b1915a..7b8587144631cf19f430377687f00cb8b1f77559 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 a444df3e87f15f6a97f1cf49eae981a611f54bd7..6b9c19c4841d89fa53bc2abb31d8d670bf602ea7 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 8d3043f76ff32eac073318772a4e75b7db17b71b..1c5e1a3f708871e8e2397fac76d011377795fe40 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, -- 2.54.0 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 7b8587144631cf19f430377687f00cb8b1f77559..cfcab92bd4f145e831b159069b9b27e5838349a2 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 6b9c19c4841d89fa53bc2abb31d8d670bf602ea7..3ef1006b0a5fd42abb07e49e96cbd38f6d4c374d 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 d6d1b2db84d60fb47d98d36c723fda6b7322f76d..67f200d3395f7109bfd777a65be6f9da76e10d46 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 1c5e1a3f708871e8e2397fac76d011377795fe40..7295b6a404d31b922ca40fb48d950ecc312b3995 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; -- 2.54.0 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 3ef1006b0a5fd42abb07e49e96cbd38f6d4c374d..597ec88951ada44bd0ca23a8ed258ac23c77be54 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)); } } -- 2.54.0 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 0feaf69d67803392b8d55ab77888c1e001c6c896..6b4c09845c78fcbf89a1ef8080a48c15bfbdb363 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 cfcab92bd4f145e831b159069b9b27e5838349a2..6e614b8006318512a6828e6a91f6ae29052603cf 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 597ec88951ada44bd0ca23a8ed258ac23c77be54..5bb1ef38db928726b5d720610b62cf4bb553f451 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; -- 2.54.0 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 | 46 ++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 5bb1ef38db928726b5d720610b62cf4bb553f451..98a2cb7fae63b8acf90bcd0462a0c58890a536c6 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(); + // Now, let's use tempdir + var tmp = tmpDir(.{}); + // defer tmp.cleanup(); - // create file - try tmp.dir.writeFile("file.txt", "nonsense"); + // Create some targets + try tmp.dir.writeFile("file.txt", "nonsense"); + try tmp.dir.makeDir("subdir"); - // get paths - // TODO: use Dir's realpath function once that exists - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); + // 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 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" }); + 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" { -- 2.54.0 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 | 49 ++++++++++++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index 6e614b8006318512a6828e6a91f6ae29052603cf..5243c1155a6c975328df23645ec286b767d3f9cc 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 98a2cb7fae63b8acf90bcd0462a0c58890a536c6..056d06d975420cd657dfbc03c5a212195a9a61a8 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}); + + // 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 = 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(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, .{ .is_directory = is_dir }); - - // 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)); +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" { -- 2.54.0 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 5243c1155a6c975328df23645ec286b767d3f9cc..e638e9f2e15bed844a82efa60f810030444fb2f4 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 056d06d975420cd657dfbc03c5a212195a9a61a8..65179e579f91e5716b245d8c7df13d42cfb3946b 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 67f200d3395f7109bfd777a65be6f9da76e10d46..ab8705048a328066c73644ccdceb8b2bb22b9077 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; } -- 2.54.0 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 e638e9f2e15bed844a82efa60f810030444fb2f4..8f86488c0eea37b99378bd7847be8baab245ac50 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 ab8705048a328066c73644ccdceb8b2bb22b9077..82aea077c56098bdcb5b0272d720df8bd06b9058 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; -- 2.54.0 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 af494efbab2de1f6ada9a07626c5c7fe087f9bde..6949388120a8c48d4b717ed2956f5150caa18ae3 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); } -- 2.54.0 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 6949388120a8c48d4b717ed2956f5150caa18ae3..8e4f92e5eec7e00454af843baf908aeb028ec9d7 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, -- 2.54.0 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 6b4c09845c78fcbf89a1ef8080a48c15bfbdb363..0c7f8f06d5e0549c5872dda0ab8ceb88a644aeab 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 a3cf2e800268cba5c8a4317f01843ddf8364d39a..c20cb2e62fdfcfd6933e5530c717277708599b22 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 8f86488c0eea37b99378bd7847be8baab245ac50..ebe457fe4d3b9135236697fc2bbad647c1ee5b1c 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 65179e579f91e5716b245d8c7df13d42cfb3946b..0d9420d389d9b5f97d1e6931d60225dfcc6e9add 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 82aea077c56098bdcb5b0272d720df8bd06b9058..8576acc384550f4bdd4b7466162ed2cbfc6418f0 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); } -- 2.54.0 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 0c7f8f06d5e0549c5872dda0ab8ceb88a644aeab..3cdb163f38478eb33c87e3311e2190ba78e5f8cc 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 0d9420d389d9b5f97d1e6931d60225dfcc6e9add..c95af79e026e07c9a238777e762d9cdcb5d30863 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 8576acc384550f4bdd4b7466162ed2cbfc6418f0..7835742e39dbb1bcaf96a2d0da9b8bd90b435433 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, -- 2.54.0 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 3cdb163f38478eb33c87e3311e2190ba78e5f8cc..a22b15b0fb9aeda2c9f7a649bd5a2cef43720885 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 c20cb2e62fdfcfd6933e5530c717277708599b22..4fecf3c9c539359adb89421331e79d4bd10a38bd 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 ebe457fe4d3b9135236697fc2bbad647c1ee5b1c..86a5a7ad08349527d849e63f0f78ad3d71027798 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 7835742e39dbb1bcaf96a2d0da9b8bd90b435433..3895e5d80a78c7e0b074c7bdc13f7dac300e91ba 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, -- 2.54.0 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 a22b15b0fb9aeda2c9f7a649bd5a2cef43720885..6e0a9defd7b11ad052364fdbc887a042d4c7e5e4 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 86a5a7ad08349527d849e63f0f78ad3d71027798..36f7669f666e39f39546c7e6c14a0d75f07bf977 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, -- 2.54.0 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 3895e5d80a78c7e0b074c7bdc13f7dac300e91ba..fcfe45e246c7b80fab44f8fa9fc1f893a965e917 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 7295b6a404d31b922ca40fb48d950ecc312b3995..9f50570e3e46d067f26530762a664adffb47db79 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; -- 2.54.0 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 fcfe45e246c7b80fab44f8fa9fc1f893a965e917..f3f5eca450d1143885603bd75c24528bbf0c1469 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 -- 2.54.0 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 6e0a9defd7b11ad052364fdbc887a042d4c7e5e4..830a4ac7cdad8395f4c43eac605af563d048c6fb 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 c95af79e026e07c9a238777e762d9cdcb5d30863..bb0893ab4c1c1784a2de9614eee720df5ffff51e 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 f3f5eca450d1143885603bd75c24528bbf0c1469..219b07dbc06efabe1daec3cc3198f5d32c91f297 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 NtCreateSymbolicLinkW(dir: ?HANDLE, sym_link_path: [:0]const u16, target_path: [:0]const u16, is_directory: bool) CreateSymbolicLinkError!void { +pub fn CreateSymbolicLink( + dir: ?HANDLE, + sym_link_path: []const u8, + target_path: []const u8, + is_directory: bool, +) CreateSymbolicLinkError!void { + const sym_link_path_w = try sliceToPrefixedFileW(sym_link_path); + const target_path_w = try sliceToPrefixedFileW(target_path); + return CreateSymbolicLinkW(dir, sym_link_path_w.span(), target_path_w.span(), is_directory); +} + +pub fn CreateSymbolicLinkW( + dir: ?HANDLE, + sym_link_path: [:0]const u16, + target_path: [:0]const u16, + 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, -- 2.54.0 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 36f7669f666e39f39546c7e6c14a0d75f07bf977..71bf34acfe805e2f56bfad2c82a9c36316e8ef9b 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 219b07dbc06efabe1daec3cc3198f5d32c91f297..ab549a0ebf73dbd9119947d9b59b1db8975d46d5 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), - } -} -- 2.54.0 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 bb0893ab4c1c1784a2de9614eee720df5ffff51e..7310562d64dcdc590cbb280c5afac4ce9ac6da22 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; -- 2.54.0 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 830a4ac7cdad8395f4c43eac605af563d048c6fb..9070fd0a985b2d912c8f8fabba2f735196f98346 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 4fecf3c9c539359adb89421331e79d4bd10a38bd..c9f171196776080c6c05751ae9212a7730dd0b52 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 71bf34acfe805e2f56bfad2c82a9c36316e8ef9b..3e05ac41e53cf4bc40b65cfeeb8669cbb5c79073 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, -- 2.54.0