diff --git a/lib/std/os.zig b/lib/std/os.zig index 24de1d83f2..e3e44b6a30 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 826be1cc8e..a444df3e87 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 0a850a25c4..d6d1b2db84 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 66ea5bb512..8d3043f76f 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;