@@ -1157,9 +1157,9 @@ pub fn GetFinalPathNameByHandle(
1157
1157
1158
1158
// This surprising path is a filesystem path to the mount manager on Windows.
1159
1159
// Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
1160
- const mgmt_path = " \\ MountPointManager" ;
1161
- const mgmt_path_u16 = sliceToPrefixedFileW ( mgmt_path ) catch unreachable ;
1162
- const mgmt_handle = OpenFile (mgmt_path_u16 . span () , .{
1160
+ // This is the NT namespaced version of \\.\ MountPointManager
1161
+ const mgmt_path_u16 = std . unicode . utf8ToUtf16LeStringLiteral ( " \\ ?? \\ MountPointManager" ) ;
1162
+ const mgmt_handle = OpenFile (mgmt_path_u16 , .{
1163
1163
.access_mask = SYNCHRONIZE ,
1164
1164
.share_access = FILE_SHARE_READ | FILE_SHARE_WRITE ,
1165
1165
.creation = FILE_OPEN ,
@@ -1997,43 +1997,248 @@ pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace {
1997
1997
return sliceToPrefixedFileW (mem .sliceTo (s , 0 ));
1998
1998
}
1999
1999
2000
- /// Converts the path `s` to WTF16, null-terminated. If the path is absolute,
2001
- /// it will get NT-style prefix `\??\` prepended automatically.
2002
- pub fn sliceToPrefixedFileW (s : []const u8 ) ! PathSpace {
2003
- // TODO https://github.com/ziglang/zig/issues/2765
2004
- var path_space : PathSpace = undefined ;
2005
- const prefix = "\\ ??\\ " ;
2006
- const prefix_index : usize = if (mem .startsWith (u8 , s , prefix )) prefix .len else 0 ;
2007
- for (s [prefix_index .. ]) | byte | {
2008
- switch (byte ) {
2009
- '*' , '?' , '"' , '<' , '>' , '|' = > return error .BadPathName ,
2010
- else = > {},
2011
- }
2012
- }
2013
- const prefix_u16 = [_ ]u16 { '\\ ' , '?' , '?' , '\\ ' };
2014
- const start_index = if (prefix_index > 0 or ! std .fs .path .isAbsolute (s )) 0 else blk : {
2015
- path_space .data [0.. prefix_u16 .len ].* = prefix_u16 ;
2016
- break :blk prefix_u16 .len ;
2017
- };
2018
- path_space .len = start_index + try std .unicode .utf8ToUtf16Le (path_space .data [start_index .. ], s );
2019
- if (path_space .len > path_space .data .len ) return error .NameTooLong ;
2020
- path_space .len = start_index + (normalizePath (u16 , path_space .data [start_index .. path_space .len ]) catch | err | switch (err ) {
2021
- error .TooManyParentDirs = > {
2022
- if (! std .fs .path .isAbsolute (s )) {
2023
- var temp_path : PathSpace = undefined ;
2024
- temp_path .len = try std .unicode .utf8ToUtf16Le (& temp_path .data , s );
2025
- std .debug .assert (temp_path .len == path_space .len );
2026
- temp_path .data [path_space .len ] = 0 ;
2027
- path_space .len = prefix_u16 .len + try getFullPathNameW (& temp_path .data , path_space .data [prefix_u16 .len .. ]);
2028
- path_space .data [0.. prefix_u16 .len ].* = prefix_u16 ;
2029
- std .debug .assert (path_space .data [path_space .len ] == 0 );
2000
+ /// Same as `wToPrefixedFileW` but accepts a UTF-8 encoded path.
2001
+ pub fn sliceToPrefixedFileW (path : []const u8 ) ! PathSpace {
2002
+ var temp_path : PathSpace = undefined ;
2003
+ temp_path .len = try std .unicode .utf8ToUtf16Le (& temp_path .data , path );
2004
+ temp_path .data [temp_path .len ] = 0 ;
2005
+ return wToPrefixedFileW (temp_path .span ());
2006
+ }
2007
+
2008
+ /// Converts the `path` to WTF16, null-terminated. If the path contains any
2009
+ /// namespace prefix, or is anything but a relative path (rooted, drive relative,
2010
+ /// etc) the result will have the NT-style prefix `\??\`.
2011
+ ///
2012
+ /// Similar to RtlDosPathNameToNtPathName_U with a few differences:
2013
+ /// - Does not allocate on the heap.
2014
+ /// - Relative paths are kept as relative unless they contain too many ..
2015
+ /// components, in which case they are treated as drive-relative and resolved
2016
+ /// against the CWD.
2017
+ /// - Special case device names like COM1, NUL, etc are not handled specially (TODO)
2018
+ /// - . and space are not stripped from the end of relative paths (potential TODO)
2019
+ pub fn wToPrefixedFileW (path : [:0 ]const u16 ) ! PathSpace {
2020
+ const nt_prefix = [_ ]u16 { '\\ ' , '?' , '?' , '\\ ' };
2021
+ switch (getNamespacePrefix (u16 , path )) {
2022
+ // TODO: Figure out a way to design an API that can avoid the copy for .nt,
2023
+ // since it is always returned fully unmodified.
2024
+ .nt , .verbatim = > {
2025
+ var path_space : PathSpace = undefined ;
2026
+ path_space .data [0.. nt_prefix .len ].* = nt_prefix ;
2027
+ const len_after_prefix = path .len - nt_prefix .len ;
2028
+ @memcpy (path_space .data [nt_prefix .len .. ][0.. len_after_prefix ], path [nt_prefix .len .. ]);
2029
+ path_space .len = path .len ;
2030
+ path_space .data [path_space .len ] = 0 ;
2031
+ return path_space ;
2032
+ },
2033
+ .local_device , .fake_verbatim = > {
2034
+ var path_space : PathSpace = undefined ;
2035
+ const path_byte_len = ntdll .RtlGetFullPathName_U (
2036
+ path .ptr ,
2037
+ path_space .data .len * 2 ,
2038
+ & path_space .data ,
2039
+ null ,
2040
+ );
2041
+ if (path_byte_len == 0 ) {
2042
+ // TODO: This may not be the right error
2043
+ return error .BadPathName ;
2044
+ } else if (path_byte_len / 2 > path_space .data .len ) {
2045
+ return error .NameTooLong ;
2046
+ }
2047
+ path_space .len = path_byte_len / 2 ;
2048
+ // Both prefixes will be normalized but retained, so all
2049
+ // we need to do now is replace them with the NT prefix
2050
+ path_space .data [0.. nt_prefix .len ].* = nt_prefix ;
2051
+ return path_space ;
2052
+ },
2053
+ .none = > {
2054
+ const path_type = getUnprefixedPathType (u16 , path );
2055
+ var path_space : PathSpace = undefined ;
2056
+ relative : {
2057
+ if (path_type == .relative ) {
2058
+ // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
2059
+ // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
2060
+
2061
+ // TODO: Potentially strip all trailing . and space characters from the
2062
+ // end of the path. This is something that both RtlDosPathNameToNtPathName_U
2063
+ // and RtlGetFullPathName_U do. Technically, trailing . and spaces
2064
+ // are allowed, but such paths may not interact well with Windows (i.e.
2065
+ // files with these paths can't be deleted from explorer.exe, etc).
2066
+ // This could be something that normalizePath may want to do.
2067
+
2068
+ @memcpy (path_space .data [0.. path .len ], path );
2069
+ // Try to normalize, but if we get too many parent directories,
2070
+ // then this is effectively a 'drive relative' path, so we need to
2071
+ // start over and use RtlGetFullPathName_U instead.
2072
+ path_space .len = normalizePath (u16 , path_space .data [0.. path .len ]) catch | err | switch (err ) {
2073
+ error .TooManyParentDirs = > break :relative ,
2074
+ };
2075
+ path_space .data [path_space .len ] = 0 ;
2076
+ return path_space ;
2077
+ }
2078
+ }
2079
+ // We now know we are going to return an absolute NT path, so
2080
+ // we can unconditionally prefix it with the NT prefix.
2081
+ path_space .data [0.. nt_prefix .len ].* = nt_prefix ;
2082
+ if (path_type == .root_local_device ) {
2083
+ // `\\.` and `\\?` always get converted to `\??\` exactly, so
2084
+ // we can just stop here
2085
+ path_space .len = nt_prefix .len ;
2086
+ path_space .data [path_space .len ] = 0 ;
2030
2087
return path_space ;
2031
2088
}
2032
- return error .BadPathName ;
2089
+ const path_buf_offset = switch (path_type ) {
2090
+ // UNC paths will always start with `\\`. However, we want to
2091
+ // end up with something like `\??\UNC\server\share`, so to get
2092
+ // RtlGetFullPathName to write into the spot we want the `server`
2093
+ // part to end up, we need to provide an offset such that
2094
+ // the `\\` part gets written where the `C\` of `UNC\` will be
2095
+ // in the final NT path.
2096
+ .unc_absolute = > nt_prefix .len + 2 ,
2097
+ else = > nt_prefix .len ,
2098
+ };
2099
+ const buf_len = @intCast (u32 , path_space .data .len - path_buf_offset );
2100
+ const path_byte_len = ntdll .RtlGetFullPathName_U (
2101
+ path .ptr ,
2102
+ buf_len * 2 ,
2103
+ path_space .data [path_buf_offset .. ].ptr ,
2104
+ null ,
2105
+ );
2106
+ if (path_byte_len == 0 ) {
2107
+ // TODO: This may not be the right error
2108
+ return error .BadPathName ;
2109
+ } else if (path_byte_len / 2 > buf_len ) {
2110
+ return error .NameTooLong ;
2111
+ }
2112
+ path_space .len = path_buf_offset + (path_byte_len / 2 );
2113
+ if (path_type == .unc_absolute ) {
2114
+ // Now add in the UNC, the `C` should overwrite the first `\` of the
2115
+ // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
2116
+ std .debug .assert (path_space .data [path_buf_offset ] == '\\ ' );
2117
+ std .debug .assert (path_space .data [path_buf_offset + 1 ] == '\\ ' );
2118
+ const unc = [_ ]u16 { 'U' , 'N' , 'C' };
2119
+ path_space .data [nt_prefix .len .. ][0.. unc .len ].* = unc ;
2120
+ }
2121
+ return path_space ;
2033
2122
},
2034
- });
2035
- path_space .data [path_space .len ] = 0 ;
2036
- return path_space ;
2123
+ }
2124
+ }
2125
+
2126
+ pub const NamespacePrefix = enum {
2127
+ none ,
2128
+ /// `\\.\` (path separators can be `\` or `/`)
2129
+ local_device ,
2130
+ /// `\\?\`
2131
+ /// When converted to an NT path, everything past the prefix is left
2132
+ /// untouched and `\\?\` is replaced by `\??\`.
2133
+ verbatim ,
2134
+ /// `\\?\` without all path separators being `\`.
2135
+ /// This seems to be recognized as a prefix, but the 'verbatim' aspect
2136
+ /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
2137
+ /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
2138
+ /// be treated as part of the final path])
2139
+ fake_verbatim ,
2140
+ /// `\??\`
2141
+ nt ,
2142
+ };
2143
+
2144
+ pub fn getNamespacePrefix (comptime T : type , path : []const T ) NamespacePrefix {
2145
+ if (path .len < 4 ) return .none ;
2146
+ var all_backslash = switch (path [0 ]) {
2147
+ '\\ ' = > true ,
2148
+ '/' = > false ,
2149
+ else = > return .none ,
2150
+ };
2151
+ all_backslash = all_backslash and switch (path [3 ]) {
2152
+ '\\ ' = > true ,
2153
+ '/' = > false ,
2154
+ else = > return .none ,
2155
+ };
2156
+ switch (path [1 ]) {
2157
+ '?' = > if (path [2 ] == '?' and all_backslash ) return .nt else return .none ,
2158
+ '\\ ' = > {},
2159
+ '/' = > all_backslash = false ,
2160
+ else = > return .none ,
2161
+ }
2162
+ return switch (path [2 ]) {
2163
+ '?' = > if (all_backslash ) .verbatim else .fake_verbatim ,
2164
+ '.' = > .local_device ,
2165
+ else = > .none ,
2166
+ };
2167
+ }
2168
+
2169
+ test getNamespacePrefix {
2170
+ try std .testing .expectEqual (NamespacePrefix .none , getNamespacePrefix (u8 , "" ));
2171
+ try std .testing .expectEqual (NamespacePrefix .nt , getNamespacePrefix (u8 , "\\ ??\\ " ));
2172
+ try std .testing .expectEqual (NamespacePrefix .none , getNamespacePrefix (u8 , "/??/" ));
2173
+ try std .testing .expectEqual (NamespacePrefix .none , getNamespacePrefix (u8 , "/??\\ " ));
2174
+ try std .testing .expectEqual (NamespacePrefix .none , getNamespacePrefix (u8 , "\\ ?\\\\ " ));
2175
+ try std .testing .expectEqual (NamespacePrefix .local_device , getNamespacePrefix (u8 , "\\\\ .\\ " ));
2176
+ try std .testing .expectEqual (NamespacePrefix .local_device , getNamespacePrefix (u8 , "\\\\ ./" ));
2177
+ try std .testing .expectEqual (NamespacePrefix .local_device , getNamespacePrefix (u8 , "/\\ ./" ));
2178
+ try std .testing .expectEqual (NamespacePrefix .local_device , getNamespacePrefix (u8 , "//./" ));
2179
+ try std .testing .expectEqual (NamespacePrefix .none , getNamespacePrefix (u8 , "/.//" ));
2180
+ try std .testing .expectEqual (NamespacePrefix .verbatim , getNamespacePrefix (u8 , "\\\\ ?\\ " ));
2181
+ try std .testing .expectEqual (NamespacePrefix .fake_verbatim , getNamespacePrefix (u8 , "\\ /?\\ " ));
2182
+ try std .testing .expectEqual (NamespacePrefix .fake_verbatim , getNamespacePrefix (u8 , "\\ /?/" ));
2183
+ try std .testing .expectEqual (NamespacePrefix .fake_verbatim , getNamespacePrefix (u8 , "//?/" ));
2184
+ }
2185
+
2186
+ pub const UnprefixedPathType = enum {
2187
+ unc_absolute ,
2188
+ drive_absolute ,
2189
+ drive_relative ,
2190
+ rooted ,
2191
+ relative ,
2192
+ root_local_device ,
2193
+ };
2194
+
2195
+ inline fn isSepW (c : u16 ) bool {
2196
+ return c == '/' or c == '\\ ' ;
2197
+ }
2198
+
2199
+ /// Get the path type of a path that is known to not have any namespace prefixes
2200
+ /// (`\\?\`, `\\.\`, `\??\`).
2201
+ pub fn getUnprefixedPathType (comptime T : type , path : []const T ) UnprefixedPathType {
2202
+ if (path .len < 1 ) return .relative ;
2203
+
2204
+ if (std .debug .runtime_safety ) {
2205
+ std .debug .assert (getNamespacePrefix (T , path ) == .none );
2206
+ }
2207
+
2208
+ if (isSepW (path [0 ])) {
2209
+ // \x
2210
+ if (path .len < 2 or ! isSepW (path [1 ])) return .rooted ;
2211
+ // exactly \\. or \\? with nothing trailing
2212
+ if (path .len == 3 and (path [2 ] == '.' or path [2 ] == '?' )) return .root_local_device ;
2213
+ // \\x
2214
+ return .unc_absolute ;
2215
+ } else {
2216
+ // x
2217
+ if (path .len < 2 or path [1 ] != ':' ) return .relative ;
2218
+ // x:\
2219
+ if (path .len > 2 and isSepW (path [2 ])) return .drive_absolute ;
2220
+ // x:
2221
+ return .drive_relative ;
2222
+ }
2223
+ }
2224
+
2225
+ test getUnprefixedPathType {
2226
+ try std .testing .expectEqual (UnprefixedPathType .relative , getUnprefixedPathType (u8 , "" ));
2227
+ try std .testing .expectEqual (UnprefixedPathType .relative , getUnprefixedPathType (u8 , "x" ));
2228
+ try std .testing .expectEqual (UnprefixedPathType .relative , getUnprefixedPathType (u8 , "x\\ " ));
2229
+ try std .testing .expectEqual (UnprefixedPathType .root_local_device , getUnprefixedPathType (u8 , "//." ));
2230
+ try std .testing .expectEqual (UnprefixedPathType .root_local_device , getUnprefixedPathType (u8 , "/\\ ?" ));
2231
+ try std .testing .expectEqual (UnprefixedPathType .root_local_device , getUnprefixedPathType (u8 , "\\\\ ?" ));
2232
+ try std .testing .expectEqual (UnprefixedPathType .unc_absolute , getUnprefixedPathType (u8 , "\\\\ x" ));
2233
+ try std .testing .expectEqual (UnprefixedPathType .unc_absolute , getUnprefixedPathType (u8 , "//x" ));
2234
+ try std .testing .expectEqual (UnprefixedPathType .rooted , getUnprefixedPathType (u8 , "\\ x" ));
2235
+ try std .testing .expectEqual (UnprefixedPathType .rooted , getUnprefixedPathType (u8 , "/" ));
2236
+ try std .testing .expectEqual (UnprefixedPathType .drive_relative , getUnprefixedPathType (u8 , "x:" ));
2237
+ try std .testing .expectEqual (UnprefixedPathType .drive_relative , getUnprefixedPathType (u8 , "x:abc" ));
2238
+ try std .testing .expectEqual (UnprefixedPathType .drive_relative , getUnprefixedPathType (u8 , "x:a/b/c" ));
2239
+ try std .testing .expectEqual (UnprefixedPathType .drive_absolute , getUnprefixedPathType (u8 , "x:\\ " ));
2240
+ try std .testing .expectEqual (UnprefixedPathType .drive_absolute , getUnprefixedPathType (u8 , "x:\\ abc" ));
2241
+ try std .testing .expectEqual (UnprefixedPathType .drive_absolute , getUnprefixedPathType (u8 , "x:/a/b/c" ));
2037
2242
}
2038
2243
2039
2244
fn getFullPathNameW (path : [* :0 ]const u16 , out : []u16 ) ! usize {
@@ -2046,34 +2251,6 @@ fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
2046
2251
return result ;
2047
2252
}
2048
2253
2049
- /// Assumes an absolute path.
2050
- pub fn wToPrefixedFileW (s : []const u16 ) ! PathSpace {
2051
- // TODO https://github.com/ziglang/zig/issues/2765
2052
- var path_space : PathSpace = undefined ;
2053
-
2054
- const start_index = if (mem .startsWith (u16 , s , &[_ ]u16 { '\\ ' , '?' })) 0 else blk : {
2055
- const prefix = [_ ]u16 { '\\ ' , '?' , '?' , '\\ ' };
2056
- path_space .data [0.. prefix .len ].* = prefix ;
2057
- break :blk prefix .len ;
2058
- };
2059
- path_space .len = start_index + s .len ;
2060
- if (path_space .len > path_space .data .len ) return error .NameTooLong ;
2061
- @memcpy (path_space .data [start_index .. ][0.. s .len ], s );
2062
- // > File I/O functions in the Windows API convert "/" to "\" as part of
2063
- // > converting the name to an NT-style name, except when using the "\\?\"
2064
- // > prefix as detailed in the following sections.
2065
- // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation
2066
- // Because we want the larger maximum path length for absolute paths, we
2067
- // convert forward slashes to backward slashes here.
2068
- for (path_space .data [0.. path_space .len ]) | * elem | {
2069
- if (elem .* == '/' ) {
2070
- elem .* = '\\ ' ;
2071
- }
2072
- }
2073
- path_space .data [path_space .len ] = 0 ;
2074
- return path_space ;
2075
- }
2076
-
2077
2254
inline fn MAKELANGID (p : c_ushort , s : c_ushort ) LANGID {
2078
2255
return (s << 10 ) | p ;
2079
2256
}
0 commit comments