Commit 666584067a

Jonathan Marler <johnnymarler@gmail.com>
2021-01-03 12:19:24
implement nt path conversion for windows
1 parent 34d0542
Changed files (5)
lib/std/fs/test.zig
@@ -79,8 +79,17 @@ test "openDirAbsolute" {
         break :blk try fs.realpathAlloc(&arena.allocator, relative_path);
     };
 
-    var dir = try fs.openDirAbsolute(base_path, .{});
-    defer dir.close();
+    {
+        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 "readLinkAbsolute" {
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)];
+    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);
+    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");
+}
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.collapseRepeats(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 {
@@ -1749,17 +1824,9 @@ pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace {
     };
     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 => return error.BadPathName,
+    });
     path_space.data[path_space.len] = 0;
     return path_space;
 }
@@ -1864,3 +1931,9 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError {
     }
     return error.Unexpected;
 }
+
+test "" {
+    if (builtin.os.tag == .windows) {
+        _ = @import("windows/test.zig");
+    }
+}
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(
lib/std/mem.zig
@@ -2120,6 +2120,49 @@ 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 collapseRepeats(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;
+}
+
+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);
+    const actual = mutable[0..collapseRepeats(u8, mutable, elem)];
+    testing.expect(std.mem.eql(u8, actual, 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 {