diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 2e3d4fca053d..a280f6ccaaab 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1365,15 +1365,6 @@ pub const Dir = struct { .SecurityDescriptor = null, .SecurityQualityOfService = null, }; - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - // Windows does not recognize this, but it does work with empty string. - nt_name.Length = 0; - } - if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { - // If you're looking to contribute to zig and fix this, see here for an example of how to - // 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( diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index f586c50b6a3d..bcf4bf5c9796 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -79,7 +79,23 @@ test "openDirAbsolute" { break :blk try fs.realpathAlloc(&arena.allocator, relative_path); }; - var dir = try fs.openDirAbsolute(base_path, .{}); + { + var dir = try fs.openDirAbsolute(base_path, .{}); + defer dir.close(); + } + + for ([_][]const u8{ ".", ".." }) |sub_path| { + const dir_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, sub_path }); + defer arena.allocator.free(dir_path); + var dir = try fs.openDirAbsolute(dir_path, .{}); + defer dir.close(); + } +} + +test "openDir cwd parent .." { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + var dir = try fs.cwd().openDir("..", .{}); defer dir.close(); } diff --git a/lib/std/mem.zig b/lib/std/mem.zig index 044d73413a7e..56391bcfe1e8 100644 --- a/lib/std/mem.zig +++ b/lib/std/mem.zig @@ -2120,6 +2120,53 @@ test "replace" { try testing.expectEqualStrings(expected, output[0..expected.len]); } +/// Replace all occurences of `needle` with `replacement`. +pub fn replaceScalar(comptime T: type, slice: []T, needle: T, replacement: T) void { + for (slice) |e, i| { + if (e == needle) { + slice[i] = replacement; + } + } +} + +/// Collapse consecutive duplicate elements into one entry. +pub fn collapseRepeatsLen(comptime T: type, slice: []T, elem: T) usize { + if (slice.len == 0) return 0; + var write_idx: usize = 1; + var read_idx: usize = 1; + while (read_idx < slice.len) : (read_idx += 1) { + if (slice[read_idx - 1] != elem or slice[read_idx] != elem) { + slice[write_idx] = slice[read_idx]; + write_idx += 1; + } + } + return write_idx; +} + +/// Collapse consecutive duplicate elements into one entry. +pub fn collapseRepeats(comptime T: type, slice: []T, elem: T) []T { + return slice[0 .. collapseRepeatsLen(T, slice, elem)]; +} + +fn testCollapseRepeats(str: []const u8, elem: u8, expected: []const u8) !void { + const mutable = try std.testing.allocator.dupe(u8, str); + defer std.testing.allocator.free(mutable); + try testing.expect(std.mem.eql(u8, collapseRepeats(u8, mutable, elem), expected)); +} +test "collapseRepeats" { + try testCollapseRepeats("", '/', ""); + try testCollapseRepeats("a", '/', "a"); + try testCollapseRepeats("/", '/', "/"); + try testCollapseRepeats("//", '/', "/"); + try testCollapseRepeats("/a", '/', "/a"); + try testCollapseRepeats("//a", '/', "/a"); + try testCollapseRepeats("a/", '/', "a/"); + try testCollapseRepeats("a//", '/', "a/"); + try testCollapseRepeats("a/a", '/', "a/a"); + try testCollapseRepeats("a//a", '/', "a/a"); + try testCollapseRepeats("//a///a////", '/', "/a/a/"); +} + /// Calculate the size needed in an output buffer to perform a replacement. /// The needle must not be empty. pub fn replacementSize(comptime T: type, input: []const T, needle: []const T, replacement: []const T) usize { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 9d898dadaf92..e02cf4e28052 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1723,6 +1723,81 @@ pub const PathSpace = struct { } }; +/// The error type for `removeDotDirsSanitized` +pub const RemoveDotDirsError = error{TooManyParentDirs}; + +/// Removes '.' and '..' path components from a "sanitized relative path". +/// A "sanitized path" is one where: +/// 1) all forward slashes have been replaced with back slashes +/// 2) all repeating back slashes have been collapsed +/// 3) the path is a relative one (does not start with a back slash) +pub fn removeDotDirsSanitized(comptime T: type, path: []T) RemoveDotDirsError!usize { + std.debug.assert(path.len == 0 or path[0] != '\\'); + + var write_idx: usize = 0; + var read_idx: usize = 0; + while (read_idx < path.len) { + if (path[read_idx] == '.') { + if (read_idx + 1 == path.len) + return write_idx; + + const after_dot = path[read_idx + 1]; + if (after_dot == '\\') { + read_idx += 2; + continue; + } + if (after_dot == '.' and (read_idx + 2 == path.len or path[read_idx + 2] == '\\')) { + if (write_idx == 0) return error.TooManyParentDirs; + std.debug.assert(write_idx >= 2); + write_idx -= 1; + while (true) { + write_idx -= 1; + if (write_idx == 0) break; + if (path[write_idx] == '\\') { + write_idx += 1; + break; + } + } + if (read_idx + 2 == path.len) + return write_idx; + read_idx += 3; + continue; + } + } + + // skip to the next path separator + while (true) : (read_idx += 1) { + if (read_idx == path.len) + return write_idx; + path[write_idx] = path[read_idx]; + write_idx += 1; + if (path[read_idx] == '\\') + break; + } + read_idx += 1; + } + return write_idx; +} + +/// Normalizes a Windows path with the following steps: +/// 1) convert all forward slashes to back slashes +/// 2) collapse duplicate back slashes +/// 3) remove '.' and '..' directory parts +/// Returns the length of the new path. +pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize { + mem.replaceScalar(T, path, '/', '\\'); + const new_len = mem.collapseRepeatsLen(T, path, '\\'); + + const prefix_len: usize = init: { + if (new_len >= 1 and path[0] == '\\') break :init 1; + if (new_len >= 2 and path[1] == ':') + break :init if (new_len >= 3 and path[2] == '\\') @as(usize, 3) else @as(usize, 2); + break :init 0; + }; + + return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]); +} + /// Same as `sliceToPrefixedFileW` but accepts a pointer /// to a null-terminated path. pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace { @@ -1742,28 +1817,42 @@ pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace { else => {}, } } + const prefix_u16 = [_]u16{ '\\', '?', '?', '\\' }; const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: { - 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; - // > 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.len = start_index + (normalizePath(u16, path_space.data[start_index..path_space.len]) catch |err| switch (err) { + error.TooManyParentDirs => { + if (!std.fs.path.isAbsolute(s)) { + var temp_path: PathSpace = undefined; + temp_path.len = try std.unicode.utf8ToUtf16Le(&temp_path.data, s); + std.debug.assert(temp_path.len == path_space.len); + temp_path.data[path_space.len] = 0; + path_space.len = prefix_u16.len + try getFullPathNameW(&temp_path.data, path_space.data[prefix_u16.len..]); + mem.copy(u16, &path_space.data, &prefix_u16); + std.debug.assert(path_space.data[path_space.len] == 0); + return path_space; + } + return error.BadPathName; + }, + }); path_space.data[path_space.len] = 0; return path_space; } +fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize { + const result= kernel32.GetFullPathNameW(path, @intCast(u32, out.len), std.meta.assumeSentinel(out.ptr, 0), null); + if (result == 0) { + switch (kernel32.GetLastError()) { + else => |err| return unexpectedError(err), + } + } + return result; +} + /// Assumes an absolute path. pub fn wToPrefixedFileW(s: []const u16) !PathSpace { // TODO https://github.com/ziglang/zig/issues/2765 @@ -1864,3 +1953,9 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError { } return error.Unexpected; } + +test "" { + if (builtin.os.tag == .windows) { + _ = @import("windows/test.zig"); + } +} diff --git a/lib/std/os/windows/kernel32.zig b/lib/std/os/windows/kernel32.zig index e117f362eb17..f2e8b87d74c2 100644 --- a/lib/std/os/windows/kernel32.zig +++ b/lib/std/os/windows/kernel32.zig @@ -136,6 +136,13 @@ pub extern "kernel32" fn GetFinalPathNameByHandleW( dwFlags: DWORD, ) callconv(WINAPI) DWORD; +pub extern "kernel32" fn GetFullPathNameW( + lpFileName: [*:0]const u16, + nBufferLength: u32, + lpBuffer: ?[*:0]u16, + lpFilePart: ?*?[*:0]u16, +) callconv(@import("std").os.windows.WINAPI) u32; + pub extern "kernel32" fn GetOverlappedResult(hFile: HANDLE, lpOverlapped: *OVERLAPPED, lpNumberOfBytesTransferred: *DWORD, bWait: BOOL) callconv(WINAPI) BOOL; pub extern "kernel32" fn GetProcessHeap() callconv(WINAPI) ?HANDLE; diff --git a/lib/std/os/windows/test.zig b/lib/std/os/windows/test.zig new file mode 100644 index 000000000000..8c18d413ca03 --- /dev/null +++ b/lib/std/os/windows/test.zig @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2015-2020 Zig Contributors +// This file is part of [zig](https://ziglang.org/), which is MIT licensed. +// The MIT license requires this copyright notice to be included in all copies +// and substantial portions of the software. +const std = @import("../../std.zig"); +const builtin = @import("builtin"); +const windows = std.os.windows; +const mem = std.mem; +const testing = std.testing; +const expect = testing.expect; + +fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void { + const mutable = try testing.allocator.dupe(u8, str); + defer testing.allocator.free(mutable); + const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)]; + try testing.expect(mem.eql(u8, actual, expected)); +} +fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void { + const mutable = try testing.allocator.dupe(u8, str); + defer testing.allocator.free(mutable); + try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable)); +} +test "removeDotDirs" { + try testRemoveDotDirs("", ""); + try testRemoveDotDirs(".", ""); + try testRemoveDotDirs(".\\", ""); + try testRemoveDotDirs(".\\.", ""); + try testRemoveDotDirs(".\\.\\", ""); + try testRemoveDotDirs(".\\.\\.", ""); + + try testRemoveDotDirs("a", "a"); + try testRemoveDotDirs("a\\", "a\\"); + try testRemoveDotDirs("a\\b", "a\\b"); + try testRemoveDotDirs("a\\.", "a\\"); + try testRemoveDotDirs("a\\b\\.", "a\\b\\"); + try testRemoveDotDirs("a\\.\\b", "a\\b"); + + try testRemoveDotDirs(".a", ".a"); + try testRemoveDotDirs(".a\\", ".a\\"); + try testRemoveDotDirs(".a\\.b", ".a\\.b"); + try testRemoveDotDirs(".a\\.", ".a\\"); + try testRemoveDotDirs(".a\\.\\.", ".a\\"); + try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b"); + try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\"); + + try testRemoveDotDirsError(error.TooManyParentDirs, ".."); + try testRemoveDotDirsError(error.TooManyParentDirs, "..\\"); + try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\"); + try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\"); + + try testRemoveDotDirs("a\\..", ""); + try testRemoveDotDirs("a\\..\\", ""); + try testRemoveDotDirs("a\\..\\.", ""); + try testRemoveDotDirs("a\\..\\.\\", ""); + try testRemoveDotDirs("a\\..\\.\\.", ""); + try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\.."); + + try testRemoveDotDirs("a\\..\\.\\.\\b", "b"); + try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\"); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\"); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\"); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", ""); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", ""); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", ""); + try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\.."); + + try testRemoveDotDirs("a\\b\\..\\", "a\\"); + try testRemoveDotDirs("a\\b\\..\\c", "a\\c"); +}