Skip to content

Commit e9958e7

Browse files
committed
implement nt path conversion for windows
1 parent 354c14d commit e9958e7

File tree

5 files changed

+208
-22
lines changed

5 files changed

+208
-22
lines changed

lib/std/fs.zig

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,15 +1363,6 @@ pub const Dir = struct {
13631363
.SecurityDescriptor = null,
13641364
.SecurityQualityOfService = null,
13651365
};
1366-
if (sub_path_w[0] == '.' and sub_path_w[1] == 0) {
1367-
// Windows does not recognize this, but it does work with empty string.
1368-
nt_name.Length = 0;
1369-
}
1370-
if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) {
1371-
// If you're looking to contribute to zig and fix this, see here for an example of how to
1372-
// implement this: https://git.midipix.org/ntapi/tree/src/fs/ntapi_tt_open_physical_parent_directory.c
1373-
@panic("TODO opening '..' with a relative directory handle is not yet implemented on Windows");
1374-
}
13751366
const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0;
13761367
var io: w.IO_STATUS_BLOCK = undefined;
13771368
const rc = w.ntdll.NtCreateFile(

lib/std/fs/test.zig

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,17 @@ test "openDirAbsolute" {
7979
break :blk try fs.realpathAlloc(&arena.allocator, relative_path);
8080
};
8181

82-
var dir = try fs.openDirAbsolute(base_path, .{});
83-
defer dir.close();
82+
{
83+
var dir = try fs.openDirAbsolute(base_path, .{});
84+
defer dir.close();
85+
}
86+
87+
for ([_][]const u8{ ".", ".." }) |sub_path| {
88+
const dir_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, sub_path });
89+
defer arena.allocator.free(dir_path);
90+
var dir = try fs.openDirAbsolute(dir_path, .{});
91+
defer dir.close();
92+
}
8493
}
8594

8695
test "readLinkAbsolute" {

lib/std/mem.zig

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1907,6 +1907,49 @@ test "replace" {
19071907
testing.expect(eql(u8, output[0..], "Favor reading over writing ."));
19081908
}
19091909

1910+
/// Replace all occurences of `needle` with `replacement`.
1911+
pub fn replaceScalar(comptime T: type, slice: []T, needle: T, replacement: T) void {
1912+
for (slice) |e, i| {
1913+
if (e == needle) {
1914+
slice[i] = replacement;
1915+
}
1916+
}
1917+
}
1918+
1919+
/// Collapse consecutive duplicate elements into one entry.
1920+
pub fn collapseRepeats(comptime T: type, slice: []T, elem: T) usize {
1921+
if (slice.len == 0) return 0;
1922+
var write_idx: usize = 1;
1923+
var read_idx: usize = 1;
1924+
while (read_idx < slice.len) : (read_idx += 1) {
1925+
if (slice[read_idx - 1] != elem or slice[read_idx] != elem) {
1926+
slice[write_idx] = slice[read_idx];
1927+
write_idx += 1;
1928+
}
1929+
}
1930+
return write_idx;
1931+
}
1932+
1933+
fn testCollapseRepeats(str: []const u8, elem: u8, expected: []const u8) !void {
1934+
const mutable = try std.testing.allocator.dupe(u8, str);
1935+
defer std.testing.allocator.free(mutable);
1936+
const actual = mutable[0..collapseRepeats(u8, mutable, elem)];
1937+
testing.expect(std.mem.eql(u8, actual, expected));
1938+
}
1939+
test "collapseRepeats" {
1940+
try testCollapseRepeats("", '/', "");
1941+
try testCollapseRepeats("a", '/', "a");
1942+
try testCollapseRepeats("/", '/', "/");
1943+
try testCollapseRepeats("//", '/', "/");
1944+
try testCollapseRepeats("/a", '/', "/a");
1945+
try testCollapseRepeats("//a", '/', "/a");
1946+
try testCollapseRepeats("a/", '/', "a/");
1947+
try testCollapseRepeats("a//", '/', "a/");
1948+
try testCollapseRepeats("a/a", '/', "a/a");
1949+
try testCollapseRepeats("a//a", '/', "a/a");
1950+
try testCollapseRepeats("//a///a////", '/', "/a/a/");
1951+
}
1952+
19101953
/// Calculate the size needed in an output buffer to perform a replacement.
19111954
pub fn replacementSize(comptime T: type, input: []const T, needle: []const T, replacement: []const T) usize {
19121955
var i: usize = 0;

lib/std/os/windows.zig

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,81 @@ pub const PathSpace = struct {
16131613
}
16141614
};
16151615

1616+
/// The error type for `removeDotDirsSanitized`
1617+
pub const RemoveDotDirsError = error{TooManyParentDirs};
1618+
1619+
/// Removes '.' and '..' path components from a "sanitized relative path".
1620+
/// A "sanitized path" is one where:
1621+
/// 1) all forward slashes have been replaced with back slashes
1622+
/// 2) all repeating back slashes have been collapsed
1623+
/// 3) the path is a relative one (does not start with a back slash)
1624+
pub fn removeDotDirsSanitized(comptime T: type, path: []T) RemoveDotDirsError!usize {
1625+
std.debug.assert(path.len == 0 or path[0] != '\\');
1626+
1627+
var write_idx: usize = 0;
1628+
var read_idx: usize = 0;
1629+
while (read_idx < path.len) {
1630+
if (path[read_idx] == '.') {
1631+
if (read_idx + 1 == path.len)
1632+
return write_idx;
1633+
1634+
const after_dot = path[read_idx + 1];
1635+
if (after_dot == '\\') {
1636+
read_idx += 2;
1637+
continue;
1638+
}
1639+
if (after_dot == '.' and (read_idx + 2 == path.len or path[read_idx + 2] == '\\')) {
1640+
if (write_idx == 0) return error.TooManyParentDirs;
1641+
std.debug.assert(write_idx >= 2);
1642+
write_idx -= 1;
1643+
while (true) {
1644+
write_idx -= 1;
1645+
if (write_idx == 0) break;
1646+
if (path[write_idx] == '\\') {
1647+
write_idx += 1;
1648+
break;
1649+
}
1650+
}
1651+
if (read_idx + 2 == path.len)
1652+
return write_idx;
1653+
read_idx += 3;
1654+
continue;
1655+
}
1656+
}
1657+
1658+
// skip to the next path separator
1659+
while (true) : (read_idx += 1) {
1660+
if (read_idx == path.len)
1661+
return write_idx;
1662+
path[write_idx] = path[read_idx];
1663+
write_idx += 1;
1664+
if (path[read_idx] == '\\')
1665+
break;
1666+
}
1667+
read_idx += 1;
1668+
}
1669+
return write_idx;
1670+
}
1671+
1672+
/// Normalizes a Windows path with the following steps:
1673+
/// 1) convert all forward slashes to back slashes
1674+
/// 2) collapse duplicate back slashes
1675+
/// 3) remove '.' and '..' directory parts
1676+
/// Returns the length of the new path.
1677+
pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize {
1678+
mem.replaceScalar(T, path, '/', '\\');
1679+
const new_len = mem.collapseRepeats(T, path, '\\');
1680+
1681+
const prefix_len: usize = init: {
1682+
if (new_len >= 1 and path[0] == '\\') break :init 1;
1683+
if (new_len >= 2 and path[1] == ':')
1684+
break :init if (new_len >= 3 and path[2] == '\\') @as(usize, 3) else @as(usize, 2);
1685+
break :init 0;
1686+
};
1687+
1688+
return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]);
1689+
}
1690+
16161691
/// Same as `sliceToPrefixedFileW` but accepts a pointer
16171692
/// to a null-terminated path.
16181693
pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace {
@@ -1639,17 +1714,9 @@ pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace {
16391714
};
16401715
path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s);
16411716
if (path_space.len > path_space.data.len) return error.NameTooLong;
1642-
// > File I/O functions in the Windows API convert "/" to "\" as part of
1643-
// > converting the name to an NT-style name, except when using the "\\?\"
1644-
// > prefix as detailed in the following sections.
1645-
// from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation
1646-
// Because we want the larger maximum path length for absolute paths, we
1647-
// convert forward slashes to backward slashes here.
1648-
for (path_space.data[0..path_space.len]) |*elem| {
1649-
if (elem.* == '/') {
1650-
elem.* = '\\';
1651-
}
1652-
}
1717+
path_space.len = start_index + (normalizePath(u16, path_space.data[start_index..path_space.len]) catch |err| switch (err) {
1718+
error.TooManyParentDirs => return error.BadPathName,
1719+
});
16531720
path_space.data[path_space.len] = 0;
16541721
return path_space;
16551722
}
@@ -1722,3 +1789,9 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError {
17221789
}
17231790
return error.Unexpected;
17241791
}
1792+
1793+
test "" {
1794+
if (builtin.os.tag == .windows) {
1795+
_ = @import("windows/test.zig");
1796+
}
1797+
}

lib/std/os/windows/test.zig

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) 2015-2020 Zig Contributors
3+
// This file is part of [zig](https://ziglang.org/), which is MIT licensed.
4+
// The MIT license requires this copyright notice to be included in all copies
5+
// and substantial portions of the software.
6+
const std = @import("../../std.zig");
7+
const builtin = @import("builtin");
8+
const windows = std.os.windows;
9+
const mem = std.mem;
10+
const testing = std.testing;
11+
const expect = testing.expect;
12+
13+
fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
14+
const mutable = try testing.allocator.dupe(u8, str);
15+
defer testing.allocator.free(mutable);
16+
const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)];
17+
testing.expect(mem.eql(u8, actual, expected));
18+
}
19+
fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void {
20+
const mutable = try testing.allocator.dupe(u8, str);
21+
defer testing.allocator.free(mutable);
22+
testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable));
23+
}
24+
test "removeDotDirs" {
25+
try testRemoveDotDirs("", "");
26+
try testRemoveDotDirs(".", "");
27+
try testRemoveDotDirs(".\\", "");
28+
try testRemoveDotDirs(".\\.", "");
29+
try testRemoveDotDirs(".\\.\\", "");
30+
try testRemoveDotDirs(".\\.\\.", "");
31+
32+
try testRemoveDotDirs("a", "a");
33+
try testRemoveDotDirs("a\\", "a\\");
34+
try testRemoveDotDirs("a\\b", "a\\b");
35+
try testRemoveDotDirs("a\\.", "a\\");
36+
try testRemoveDotDirs("a\\b\\.", "a\\b\\");
37+
try testRemoveDotDirs("a\\.\\b", "a\\b");
38+
39+
try testRemoveDotDirs(".a", ".a");
40+
try testRemoveDotDirs(".a\\", ".a\\");
41+
try testRemoveDotDirs(".a\\.b", ".a\\.b");
42+
try testRemoveDotDirs(".a\\.", ".a\\");
43+
try testRemoveDotDirs(".a\\.\\.", ".a\\");
44+
try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b");
45+
try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\");
46+
47+
try testRemoveDotDirsError(error.TooManyParentDirs, "..");
48+
try testRemoveDotDirsError(error.TooManyParentDirs, "..\\");
49+
try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\");
50+
try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\");
51+
52+
try testRemoveDotDirs("a\\..", "");
53+
try testRemoveDotDirs("a\\..\\", "");
54+
try testRemoveDotDirs("a\\..\\.", "");
55+
try testRemoveDotDirs("a\\..\\.\\", "");
56+
try testRemoveDotDirs("a\\..\\.\\.", "");
57+
try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\..");
58+
59+
try testRemoveDotDirs("a\\..\\.\\.\\b", "b");
60+
try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\");
61+
try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\");
62+
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\");
63+
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", "");
64+
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", "");
65+
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", "");
66+
try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\..");
67+
68+
try testRemoveDotDirs("a\\b\\..\\", "a\\");
69+
try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
70+
}

0 commit comments

Comments
 (0)