mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 13:54:21 +00:00
Previously, fs.path handled a few of the Windows path types, but not all of them, and only a few of them correctly/consistently. This commit aims to make `std.fs.path` correct and consistent in handling all possible Win32 path types. This commit also slightly nudges the codebase towards a separation of Win32 paths and NT paths, as NT paths are not actually distinguishable from Win32 paths from looking at their contents alone (i.e. `\Device\Foo` could be an NT path or a Win32 rooted path, no way to tell without external context). This commit formalizes `std.fs.path` being fully concerned with Win32 paths, and having no special detection/handling of NT paths. Resources on Windows path types, and Win32 vs NT paths: - https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html - https://chrisdenton.github.io/omnipath/Overview.html - https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file API additions/changes/deprecations - `std.os.windows.getWin32PathType` was added (it is analogous to `RtlDetermineDosPathNameType_U`), while `std.os.windows.getNamespacePrefix` and `std.os.windows.getUnprefixedPathType` were deleted. `getWin32PathType` forms the basis on which the updated `std.fs.path` functions operate. - `std.fs.path.parsePath`, `std.fs.path.parsePathPosix`, and `std.fs.path.parsePathWindows` were added, while `std.fs.path.windowsParsePath` was deprecated. The new `parsePath` functions provide the "root" and the "kind" of a path, which is platform-specific. The now-deprecated `windowsParsePath` did not handle all possible path types, while the new `parsePathWindows` does. - `std.fs.path.diskDesignator` has been deprecated in favor of `std.fs.path.parsePath`, and same deal with `diskDesignatorWindows` -> `parsePathWindows` - `relativeWindows` is now a compile error when *not* targeting Windows, while `relativePosix` is now a compile error when targeting Windows. This is because those functions read/use the CWD path which will behave improperly when used from a system with different path semantics (e.g. calling `relativePosix` from a Windows system with a CWD like `C:\foo\bar` will give you a bogus result since that'd be treated as a single relative component when using POSIX semantics). This also allows `relativeWindows` to use Windows-specific APIs for getting the CWD and environment variables to cut down on allocations. - `componentIterator`/`ComponentIterator.init` have been made infallible. These functions used to be able to error on UNC paths with an empty server component, and on paths that were assumed to be NT paths, but now: + We follow the lead of `RtlDetermineDosPathNameType_U`/`RtlGetFullPathName_U` in how it treats a UNC path with an empty server name (e.g. `\\\share`) and allow it, even if it'll be invalid at the time of usage + Now that `std.fs.path` assumes paths are Win32 paths and not NT paths, we don't have to worry about NT paths Behavior changes - `std.fs.path` generally: any combinations of mixed path separators for UNC paths are universally supported, e.g. `\/server/share`, `/\server\share`, `/\server/\\//share` are all seen as equivalent UNC paths - `resolveWindows` handles all path types more appropriately/consistently. + `//` and `//foo` used to be treated as a relative path, but are now seen as UNC paths + If a rooted/drive-relative path cannot be resolved against anything more definite, the result will remain a rooted/drive-relative path. + I've created [a script to generate the results of a huge number of permutations of different path types](https://gist.github.com/squeek502/9eba7f19cad0d0d970ccafbc30f463bf) (the result of running the script is also included for anyone that'd like to vet the behavior). - `dirnameWindows` now treats the drive-relative root as the dirname of a drive-relative path with a component, e.g. `dirname("C:foo")` is now `C:`, whereas before it would return null. `dirnameWindows` also handles local device paths appropriately now. - `basenameWindows` now handles all path types more appropriately. The most notable change here is `//a` being treated as a partial UNC path now and therefore `basename` will return `""` for it, whereas before it would return `"a"` - `relativeWindows` will now do its best to resolve against the most appropriate CWD for each path, e.g. relative for `D:foo` will look at the CWD to check if the drive letter matches, and if not, look at the special environment variable `=D:` to get the shell-defined CWD for that drive, and if that doesn't exist, then it'll resolve against `D:\`. Implementation details - `resolveWindows` previously looped through the paths twice to build up the relevant info before doing the actual resolution. Now, `resolveWindows` iterates backwards once and keeps track of which paths are actually relevant using a bit set, which also allows it to break from the loop when it's no longer possible for earlier paths to matter. - A standalone test was added to test parts of `relativeWindows` since the CWD resolution logic depends on CWD information from the PEB and environment variables Edge cases worth noting - A strange piece of trivia that I found out while working on this is that it's technically possible to have a drive letter that it outside the intended A-Z range, or even outside the ASCII range entirely. Since we deal with both WTF-8 and WTF-16 paths, `path[0]`/`path[1]`/`path[2]` will not always refer to the same bits of information, so to get consistent behavior, some decision about how to deal with this edge case had to be made. I've made the choice to conform with how `RtlDetermineDosPathNameType_U` works, i.e. treat the first WTF-16 code unit as the drive letter. This means that when working with WTF-8, checking for drive-relative/drive-absolute paths is a bit more complicated. For more details, see the lengthy comment in `std.os.windows.getWin32PathType` - `relativeWindows` will now almost always be able to return either a fully-qualified absolute path or a relative path, but there's one scenario where it may return a rooted path: when the CWD gotten from the PEB is not a drive-absolute or UNC path (if that's actually feasible/possible?). An alternative approach to this scenario might be to resolve against the `HOMEDRIVE` env var if available, and/or default to `C:\` as a last resort in order to guarantee the result of `relative` is never a rooted path. - Partial UNC paths (e.g. `\\server` instead of `\\server\share`) are a bit awkward to handle, generally. Not entirely sure how best to handle them, so there may need to be another pass in the future to iron out any issues that arise. As of now the behavior is: + For `relative`, any part of a UNC disk designator is treated as the "root" and therefore isn't applicable for relative paths, e.g. calling `relative` with `\\server` and `\\server\share` will result in `\\server\share` rather than just `share` and if `relative` is called with `\\server\foo` and `\\server\bar` the result will be `\\server\bar` rather than `..\bar` + For `resolve`, any part of a UNC disk designator is also treated as the "root", but relative and rooted paths are still elligable for filling in missing portions of the disk designator, e.g. `resolve` with `\\server` and `foo` or `\foo` will result in `\\server\foo` Fixes #25703 Closes #25702
339 lines
16 KiB
Zig
339 lines
16 KiB
Zig
const std = @import("../../std.zig");
|
|
const builtin = @import("builtin");
|
|
const windows = std.os.windows;
|
|
const mem = std.mem;
|
|
const testing = std.testing;
|
|
|
|
/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing
|
|
/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW
|
|
/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation
|
|
// because it allocates.
|
|
fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !windows.PathSpace {
|
|
var out: windows.UNICODE_STRING = undefined;
|
|
const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null);
|
|
if (rc != windows.TRUE) return error.BadPathName;
|
|
defer windows.ntdll.RtlFreeUnicodeString(&out);
|
|
|
|
var path_space: windows.PathSpace = undefined;
|
|
const out_path = out.Buffer.?[0 .. out.Length / 2];
|
|
@memcpy(path_space.data[0..out_path.len], out_path);
|
|
path_space.len = out.Length / 2;
|
|
path_space.data[path_space.len] = 0;
|
|
|
|
return path_space;
|
|
}
|
|
|
|
/// Test that the Zig conversion matches the expected_path (for instances where
|
|
/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does).
|
|
fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
|
|
const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
|
|
const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path);
|
|
const actual_path = try windows.wToPrefixedFileW(null, path_utf16);
|
|
std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| {
|
|
std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) });
|
|
return e;
|
|
};
|
|
}
|
|
|
|
/// Test that the Zig conversion matches the expected_path and that the
|
|
/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does.
|
|
fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
|
|
try testToPrefixedFileNoOracle(path, expected_path);
|
|
try testToPrefixedFileOnlyOracle(path);
|
|
}
|
|
|
|
/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does.
|
|
fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
|
|
const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
|
|
const zig_result = try windows.wToPrefixedFileW(null, path_utf16);
|
|
const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16);
|
|
std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| {
|
|
std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) });
|
|
return e;
|
|
};
|
|
}
|
|
|
|
test "toPrefixedFileW" {
|
|
if (builtin.os.tag != .windows) return error.SkipZigTest;
|
|
|
|
// Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
|
|
// Note that these tests do not actually touch the filesystem or care about whether or not
|
|
// any of the paths actually exist or are otherwise valid.
|
|
|
|
// Drive Absolute
|
|
try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF");
|
|
try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\");
|
|
try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\");
|
|
// Trailing . and space characters are stripped
|
|
try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
|
|
try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
|
|
try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
|
|
try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\");
|
|
// Drive letter casing is unchanged
|
|
try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\");
|
|
|
|
// Drive Relative
|
|
// These tests depend on the CWD of the specified drive letter which can vary,
|
|
// so instead we just test that the Zig implementation matches the result of
|
|
// RtlDosPathNameToNtPathName_U.
|
|
// TODO: Setting the =X: environment variable didn't seem to affect
|
|
// RtlDosPathNameToNtPathName_U, not sure why that is but getting that
|
|
// to work could be an avenue to making these cases environment-independent.
|
|
// All -> are examples of the result if the X drive's cwd was X:\ABC
|
|
try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI
|
|
try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC
|
|
try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF
|
|
try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ
|
|
try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\
|
|
try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC
|
|
|
|
// Rooted
|
|
// These tests depend on the drive letter of the CWD which can vary, so
|
|
// instead we just test that the Zig implementation matches the result of
|
|
// RtlDosPathNameToNtPathName_U.
|
|
// TODO: Getting the CWD path, getting the drive letter from it, and using it to
|
|
// construct the expected NT paths could be an avenue to making these cases
|
|
// environment-independent and therefore able to use testToPrefixedFileWithOracle.
|
|
// All -> are examples of the result if the CWD's drive letter was X
|
|
try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF
|
|
try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\
|
|
try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF
|
|
try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF
|
|
try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ
|
|
try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\
|
|
|
|
// Relative
|
|
// These cases differ in functionality to RtlDosPathNameToNtPathName_U.
|
|
// Relative paths remain relative if they don't have enough .. components
|
|
// to error with TooManyParentDirs
|
|
try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF");
|
|
// TODO: enable this if trailing . and spaces are stripped from relative paths
|
|
//try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF");
|
|
try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF");
|
|
try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF");
|
|
// TooManyParentDirs, so resolved relative to the CWD
|
|
// All -> are examples of the result if the CWD was X:\ABC\DEF
|
|
try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI
|
|
try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\
|
|
|
|
// UNC Absolute
|
|
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
|
|
try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server");
|
|
try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share");
|
|
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC");
|
|
try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
|
|
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ");
|
|
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share");
|
|
|
|
// Local Device
|
|
try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20");
|
|
try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe");
|
|
try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
|
|
try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
|
|
try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
|
|
// Can replace the first component of the path (contrary to drive absolute and UNC absolute paths)
|
|
try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\");
|
|
try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine");
|
|
|
|
// Special-case device names
|
|
// TODO: Enable once these are supported
|
|
// more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
|
|
//try testToPrefixedFileWithOracle("COM1", "\\??\\COM1");
|
|
// Sometimes the special-cased device names are not respected
|
|
try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1");
|
|
try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1");
|
|
|
|
// Verbatim
|
|
// Left untouched except \\?\ is replaced by \??\
|
|
try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:");
|
|
try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1");
|
|
try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
|
|
try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
|
|
// NT Namespace
|
|
// Fully unmodified
|
|
try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:");
|
|
try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1");
|
|
try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
|
|
try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
|
|
|
|
// 'Fake' Verbatim
|
|
// If the prefix looks like the verbatim prefix but not all path separators in the
|
|
// prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor
|
|
// of the NT prefix.
|
|
try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC");
|
|
// 'Fake' NT
|
|
// If the prefix looks like the NT prefix but not all path separators in the prefix
|
|
// are backslashes, then it gets canonicalized and the /??/ is not dropped but
|
|
// rather treated as part of the path. In other words, the path is treated
|
|
// as a rooted path, so the final path is resolved relative to the CWD's
|
|
// drive letter.
|
|
// The -> shows an example of the result if the CWD's drive letter was X
|
|
try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC
|
|
|
|
// Root Local Device
|
|
// \\. and \\? always get converted to \??\
|
|
try testToPrefixedFileWithOracle("\\\\.", "\\??\\");
|
|
try testToPrefixedFileWithOracle("\\\\?", "\\??\\");
|
|
try testToPrefixedFileWithOracle("//?", "\\??\\");
|
|
try testToPrefixedFileWithOracle("//.", "\\??\\");
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
const RTL_PATH_TYPE = enum(c_int) {
|
|
Unknown,
|
|
UncAbsolute,
|
|
DriveAbsolute,
|
|
DriveRelative,
|
|
Rooted,
|
|
Relative,
|
|
LocalDevice,
|
|
RootLocalDevice,
|
|
};
|
|
|
|
pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
|
|
Path: [*:0]const u16,
|
|
) callconv(.winapi) RTL_PATH_TYPE;
|
|
|
|
test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
|
|
if (builtin.os.tag != .windows) return error.SkipZigTest;
|
|
|
|
var buf: std.ArrayList(u16) = .empty;
|
|
defer buf.deinit(std.testing.allocator);
|
|
|
|
var wtf8_buf: std.ArrayList(u8) = .empty;
|
|
defer wtf8_buf.deinit(std.testing.allocator);
|
|
|
|
var random = std.Random.DefaultPrng.init(std.testing.random_seed);
|
|
const rand = random.random();
|
|
|
|
for (0..1000) |_| {
|
|
buf.clearRetainingCapacity();
|
|
const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
|
|
wtf8_buf.clearRetainingCapacity();
|
|
const wtf8_len = std.unicode.calcWtf8Len(path);
|
|
try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
|
|
wtf8_buf.items.len = wtf8_len;
|
|
std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
|
|
|
|
const windows_type = RtlDetermineDosPathNameType_U(path);
|
|
const wtf16_type = windows.getWin32PathType(u16, path);
|
|
const wtf8_type = windows.getWin32PathType(u8, wtf8_buf.items);
|
|
|
|
checkPathType(windows_type, wtf16_type) catch |err| {
|
|
std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
|
|
std.debug.print("path bytes:\n", .{});
|
|
std.debug.dumpHex(std.mem.sliceAsBytes(path));
|
|
return err;
|
|
};
|
|
|
|
if (wtf16_type != wtf8_type) {
|
|
std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
|
|
std.debug.print("wtf-16 path bytes:\n", .{});
|
|
std.debug.dumpHex(std.mem.sliceAsBytes(path));
|
|
std.debug.print("wtf-8 path bytes:\n", .{});
|
|
std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
|
|
return error.Wtf8Wtf16Mismatch;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: windows.Win32PathType) !void {
|
|
const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
|
|
.unc_absolute => .UncAbsolute,
|
|
.drive_absolute => .DriveAbsolute,
|
|
.drive_relative => .DriveRelative,
|
|
.rooted => .Rooted,
|
|
.relative => .Relative,
|
|
.local_device => .LocalDevice,
|
|
.root_local_device => .RootLocalDevice,
|
|
};
|
|
if (windows_type != expected_windows_type) return error.PathTypeMismatch;
|
|
}
|
|
|
|
fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
|
|
const Choice = enum {
|
|
backslash,
|
|
slash,
|
|
control,
|
|
printable,
|
|
non_ascii,
|
|
};
|
|
|
|
const choices = rand.uintAtMostBiased(u16, 32);
|
|
|
|
for (0..choices) |_| {
|
|
const choice = rand.enumValue(Choice);
|
|
const code_unit = switch (choice) {
|
|
.backslash => '\\',
|
|
.slash => '/',
|
|
.control => switch (rand.uintAtMostBiased(u8, 0x20)) {
|
|
0x20 => '\x7F',
|
|
else => |b| b + 1, // no NUL
|
|
},
|
|
.printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
|
|
.non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
|
|
};
|
|
try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
|
|
}
|
|
|
|
try buf.append(allocator, 0);
|
|
return buf.items[0 .. buf.items.len - 1 :0];
|
|
}
|