Commit 19b219bc8a
Changed files (3)
lib
lib/std/os/test.zig
@@ -193,7 +193,7 @@ test "symlink with relative paths" {
os.windows.CreateSymbolicLink(
cwd.fd,
&[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' },
- &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
+ &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
false,
) catch |err| switch (err) {
// Symlink requires admin privileges on windows, so this test can legitimately fail.
@@ -351,7 +351,7 @@ test "readlinkat" {
os.windows.CreateSymbolicLink(
tmp.dir.fd,
&[_]u16{ 'l', 'i', 'n', 'k' },
- &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
+ &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
false,
) catch |err| switch (err) {
// Symlink requires admin privileges on windows, so this test can legitimately fail.
lib/std/os/windows.zig
@@ -704,6 +704,7 @@ pub const CreateSymbolicLinkError = error{
NameTooLong,
NoDevice,
NetworkNotFound,
+ BadPathName,
Unexpected,
};
@@ -716,7 +717,7 @@ pub const CreateSymbolicLinkError = error{
pub fn CreateSymbolicLink(
dir: ?HANDLE,
sym_link_path: []const u16,
- target_path: []const u16,
+ target_path: [:0]const u16,
is_directory: bool,
) CreateSymbolicLinkError!void {
const SYMLINK_DATA = extern struct {
@@ -745,25 +746,58 @@ pub fn CreateSymbolicLink(
};
defer CloseHandle(symlink_handle);
+ // Relevant portions of the documentation:
+ // > Relative links are specified using the following conventions:
+ // > - Root relative—for example, "\Windows\System32" resolves to "current drive:\Windows\System32".
+ // > - Current working directory–relative—for example, if the current working directory is
+ // > C:\Windows\System32, "C:File.txt" resolves to "C:\Windows\System32\File.txt".
+ // > Note: If you specify a current working directory–relative link, it is created as an absolute
+ // > link, due to the way the current working directory is processed based on the user and the thread.
+ // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
+ var is_target_absolute = false;
+ const final_target_path = target_path: {
+ switch (getNamespacePrefix(u16, target_path)) {
+ .none => switch (getUnprefixedPathType(u16, target_path)) {
+ // Rooted paths need to avoid getting put through wToPrefixedFileW
+ // (and they are treated as relative in this context)
+ // Note: It seems that rooted paths in symbolic links are relative to
+ // the drive that the symbolic exists on, not to the CWD's drive.
+ // So, if the symlink is on C:\ and the CWD is on D:\,
+ // it will still resolve the path relative to the root of
+ // the C:\ drive.
+ .rooted => break :target_path target_path,
+ else => {},
+ },
+ // Already an NT path, no need to do anything to it
+ .nt => break :target_path target_path,
+ else => {},
+ }
+ var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
+ // We do this after prefixing to ensure that drive-relative paths are treated as absolute
+ is_target_absolute = std.fs.path.isAbsoluteWindowsWTF16(prefixed_target_path.span());
+ break :target_path prefixed_target_path.span();
+ };
+
// prepare reparse data buffer
var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
- const buf_len = @sizeOf(SYMLINK_DATA) + target_path.len * 4;
+ const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4;
const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2;
+ const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path);
const symlink_data = SYMLINK_DATA{
.ReparseTag = IO_REPARSE_TAG_SYMLINK,
.ReparseDataLength = @as(u16, @intCast(buf_len - header_len)),
.Reserved = 0,
- .SubstituteNameOffset = @as(u16, @intCast(target_path.len * 2)),
- .SubstituteNameLength = @as(u16, @intCast(target_path.len * 2)),
+ .SubstituteNameOffset = @as(u16, @intCast(final_target_path.len * 2)),
+ .SubstituteNameLength = @as(u16, @intCast(final_target_path.len * 2)),
.PrintNameOffset = 0,
- .PrintNameLength = @as(u16, @intCast(target_path.len * 2)),
- .Flags = if (dir) |_| SYMLINK_FLAG_RELATIVE else 0,
+ .PrintNameLength = @as(u16, @intCast(final_target_path.len * 2)),
+ .Flags = if (!target_is_absolute) SYMLINK_FLAG_RELATIVE else 0,
};
@memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data));
- @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path)));
- const paths_start = @sizeOf(SYMLINK_DATA) + target_path.len * 2;
- @memcpy(buffer[paths_start..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path)));
+ @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
+ const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2;
+ @memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
_ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null);
}
@@ -861,12 +895,15 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin
}
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;
+ const win32_namespace_path = path: {
+ if (is_relative) break :path path;
+ const win32_path = ntToWin32Namespace(path) catch |err| switch (err) {
+ error.NameTooLong => unreachable,
+ error.NotNtPath => break :path path,
+ };
+ break :path win32_path.span();
+ };
+ const out_len = std.unicode.utf16leToUtf8(out_buffer, win32_namespace_path) catch unreachable;
return out_buffer[0..out_len];
}
@@ -2393,6 +2430,69 @@ test getUnprefixedPathType {
try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
}
+/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
+/// The possible transformations are:
+/// \??\C:\Some\Path -> C:\Some\Path
+/// \??\UNC\server\share\foo -> \\server\share\foo
+/// If the path does not have the NT namespace prefix, then `error.NotNtPath` is returned.
+///
+/// Functionality is based on the ReactOS test cases found here:
+/// https://github.com/reactos/reactos/blob/master/modules/rostests/apitests/ntdll/RtlNtPathNameToDosPathName.c
+pub fn ntToWin32Namespace(path: []const u16) !PathSpace {
+ if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
+
+ var path_space: PathSpace = undefined;
+ const namespace_prefix = getNamespacePrefix(u16, path);
+ switch (namespace_prefix) {
+ .nt => {
+ var dest_index: usize = 0;
+ var after_prefix = path[4..]; // after the `\??\`
+ // The prefix \??\UNC\ means this is a UNC path, in which case the
+ // `\??\UNC\` should be replaced by `\\` (two backslashes)
+ // TODO: the "UNC" should technically be matched case-insensitively, but
+ // it's unlikely to matter since most/all paths passed into this
+ // function will have come from the OS meaning it should have
+ // the 'canonical' uppercase UNC.
+ const is_unc = after_prefix.len >= 4 and std.mem.eql(u16, after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and std.fs.path.PathType.windows.isSep(u16, after_prefix[3]);
+ if (is_unc) {
+ path_space.data[0] = '\\';
+ dest_index += 1;
+ // We want to include the last `\` of `\??\UNC\`
+ after_prefix = path[7..];
+ }
+ @memcpy(path_space.data[dest_index..][0..after_prefix.len], after_prefix);
+ path_space.len = dest_index + after_prefix.len;
+ path_space.data[path_space.len] = 0;
+ return path_space;
+ },
+ else => return error.NotNtPath,
+ }
+}
+
+test "ntToWin32Namespace" {
+ const L = std.unicode.utf8ToUtf16LeStringLiteral;
+
+ try testNtToWin32Namespace(L("UNC"), L("\\??\\UNC"));
+ try testNtToWin32Namespace(L("\\\\"), L("\\??\\UNC\\"));
+ try testNtToWin32Namespace(L("\\\\path1"), L("\\??\\UNC\\path1"));
+ try testNtToWin32Namespace(L("\\\\path1\\path2"), L("\\??\\UNC\\path1\\path2"));
+
+ try testNtToWin32Namespace(L(""), L("\\??\\"));
+ try testNtToWin32Namespace(L("C:"), L("\\??\\C:"));
+ try testNtToWin32Namespace(L("C:\\"), L("\\??\\C:\\"));
+ try testNtToWin32Namespace(L("C:\\test"), L("\\??\\C:\\test"));
+ try testNtToWin32Namespace(L("C:\\test\\"), L("\\??\\C:\\test\\"));
+
+ try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("foo")));
+ try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("C:\\test")));
+ try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("\\\\.\\test")));
+}
+
+fn testNtToWin32Namespace(expected: []const u16, path: []const u16) !void {
+ const converted = try ntToWin32Namespace(path);
+ try std.testing.expectEqualSlices(u16, expected, converted.span());
+}
+
fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null);
if (result == 0) {
lib/std/fs.zig
@@ -1949,7 +1949,13 @@ pub const Dir = struct {
return self.symLinkWasi(target_path, sym_link_path, flags);
}
if (builtin.os.tag == .windows) {
- const target_path_w = try os.windows.sliceToPrefixedFileW(self.fd, target_path);
+ // Target path does not use sliceToPrefixedFileW because certain paths
+ // are handled differently when creating a symlink than they would be
+ // when converting to an NT namespaced path. CreateSymbolicLink in
+ // symLinkW will handle the necessary conversion.
+ var target_path_w: os.windows.PathSpace = undefined;
+ target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path);
+ target_path_w.data[target_path_w.len] = 0;
const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path);
return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);
}
@@ -1987,7 +1993,10 @@ pub const Dir = struct {
/// are null-terminated, WTF16 encoded.
pub fn symLinkW(
self: Dir,
- target_path_w: []const u16,
+ /// WTF-16, does not need to be NT-prefixed. The NT-prefixing
+ /// of this path is handled by CreateSymbolicLink.
+ target_path_w: [:0]const u16,
+ /// WTF-16, must be NT-prefixed or relative
sym_link_path_w: []const u16,
flags: SymLinkFlags,
) !void {