diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index f83456e93d..6b225b205d 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -84,6 +84,7 @@ pub fn main(init: process.Init.Minimal) !void { .io = io, .gpa = arena, .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}), + .cwd = try process.getCwdAlloc(single_threaded_arena.allocator()), }, .zig_exe = zig_exe, .env_map = try init.environ.createMap(arena), diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index f0971cfa39..5885f5cf12 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -38,7 +38,7 @@ pub fn main(init: std.process.Init.Minimal) void { } if (need_simple) { - return mainSimple() catch @panic("test failure\n"); + return mainSimple() catch @panic("test failure"); } const args = init.args.toSlice(fba.allocator()) catch @panic("unable to parse command line args"); diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 7e8c782dbd..db88a11461 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -1738,8 +1738,7 @@ pub fn pathFromRoot(b: *Build, sub_path: []const u8) []u8 { } fn pathFromCwd(b: *Build, sub_path: []const u8) []u8 { - const cwd = process.getCwdAlloc(b.allocator) catch @panic("OOM"); - return b.pathResolve(&.{ cwd, sub_path }); + return b.pathResolve(&.{ b.graph.cache.cwd, sub_path }); } pub fn pathJoin(b: *Build, paths: []const []const u8) []u8 { diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index b384ab13ff..e35beca617 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -30,6 +30,8 @@ mutex: Io.Mutex = .init, /// and usefulness of the cache for advanced use cases. prefixes_buffer: [4]Directory = undefined, prefixes_len: usize = 0, +/// Used to identify prefixes. References external memory. +cwd: []const u8, pub const Path = @import("Cache/Path.zig"); pub const Directory = @import("Cache/Directory.zig"); @@ -78,11 +80,12 @@ fn findPrefix(cache: *const Cache, file_path: []const u8) !PrefixedPath { /// Takes ownership of `resolved_path` on success. fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath { const gpa = cache.gpa; + const cwd = cache.cwd; const prefixes_slice = cache.prefixes(); var i: u8 = 1; // Start at 1 to skip over checking the null prefix. while (i < prefixes_slice.len) : (i += 1) { const p = prefixes_slice[i].path.?; - const sub_path = getPrefixSubpath(gpa, p, resolved_path) catch |err| switch (err) { + const sub_path = getPrefixSubpath(gpa, cwd, p, resolved_path) catch |err| switch (err) { error.NotASubPath => continue, else => |e| return e, }; @@ -100,10 +103,10 @@ fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath { }; } -fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 { - const relative = try std.fs.path.relative(allocator, prefix, path); - errdefer allocator.free(relative); - var component_iterator = std.fs.path.NativeComponentIterator.init(relative); +fn getPrefixSubpath(gpa: Allocator, cwd: []const u8, prefix: []const u8, path: []u8) ![]u8 { + const relative = try std.fs.path.relative(gpa, cwd, null, prefix, path); + errdefer gpa.free(relative); + var component_iterator: std.fs.path.NativeComponentIterator = .init(relative); if (component_iterator.root() != null) { return error.NotASubPath; } @@ -1307,11 +1310,14 @@ fn testGetCurrentFileTimestamp(io: Io, dir: Io.Dir) !Io.Timestamp { } test "cache file and then recall it" { - const io = std.testing.io; + const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); + const cwd = try std.process.getCwdAlloc(testing.allocator); + defer testing.allocator.free(cwd); + const temp_file = "test.txt"; const temp_manifest_dir = "temp_manifest_dir"; @@ -1331,6 +1337,7 @@ test "cache file and then recall it" { .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.createDirPathOpen(io, temp_manifest_dir, .{}), + .cwd = cwd, }; cache.addPrefix(.{ .path = null, .handle = tmp.dir }); defer cache.manifest_dir.close(io); @@ -1371,11 +1378,14 @@ test "cache file and then recall it" { } test "check that changing a file makes cache fail" { - const io = std.testing.io; + const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); + const cwd = try std.process.getCwdAlloc(testing.allocator); + defer testing.allocator.free(cwd); + const temp_file = "cache_hash_change_file_test.txt"; const temp_manifest_dir = "cache_hash_change_file_manifest_dir"; const original_temp_file_contents = "Hello, world!\n"; @@ -1397,6 +1407,7 @@ test "check that changing a file makes cache fail" { .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.createDirPathOpen(io, temp_manifest_dir, .{}), + .cwd = cwd, }; cache.addPrefix(.{ .path = null, .handle = tmp.dir }); defer cache.manifest_dir.close(io); @@ -1448,6 +1459,9 @@ test "no file inputs" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); + const cwd = try std.process.getCwdAlloc(testing.allocator); + defer testing.allocator.free(cwd); + const temp_manifest_dir = "no_file_inputs_manifest_dir"; var digest1: HexDigest = undefined; @@ -1457,6 +1471,7 @@ test "no file inputs" { .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.createDirPathOpen(io, temp_manifest_dir, .{}), + .cwd = cwd, }; cache.addPrefix(.{ .path = null, .handle = tmp.dir }); defer cache.manifest_dir.close(io); @@ -1489,11 +1504,14 @@ test "no file inputs" { } test "Manifest with files added after initial hash work" { - const io = std.testing.io; + const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); + const cwd = try std.process.getCwdAlloc(testing.allocator); + defer testing.allocator.free(cwd); + const temp_file1 = "cache_hash_post_file_test1.txt"; const temp_file2 = "cache_hash_post_file_test2.txt"; const temp_manifest_dir = "cache_hash_post_file_manifest_dir"; @@ -1516,6 +1534,7 @@ test "Manifest with files added after initial hash work" { .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.createDirPathOpen(io, temp_manifest_dir, .{}), + .cwd = cwd, }; cache.addPrefix(.{ .path = null, .handle = tmp.dir }); defer cache.manifest_dir.close(io); diff --git a/lib/std/Build/Step/Options.zig b/lib/std/Build/Step/Options.zig index 8cfa7c1261..614ff515d4 100644 --- a/lib/std/Build/Step/Options.zig +++ b/lib/std/Build/Step/Options.zig @@ -537,6 +537,9 @@ test Options { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); + const cwd = try std.process.getCwdAlloc(std.testing.allocator); + defer std.testing.allocator.free(cwd); + var graph: std.Build.Graph = .{ .io = io, .arena = arena.allocator(), @@ -544,6 +547,7 @@ test Options { .io = io, .gpa = arena.allocator(), .manifest_dir = Io.Dir.cwd(), + .cwd = cwd, }, .zig_exe = "test", .env_map = std.process.Environ.Map.init(arena.allocator()), diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index 5500d16eca..32b04b4a98 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -750,28 +750,30 @@ fn checksContainStderr(checks: []const StdIo.Check) bool { /// to make sure the child doesn't see paths relative to a cwd other than its own. fn convertPathArg(run: *Run, path: Build.Cache.Path) []const u8 { const b = run.step.owner; - const path_str = path.toString(b.graph.arena) catch @panic("OOM"); + const graph = b.graph; + const arena = graph.arena; + + const path_str = path.toString(arena) catch @panic("OOM"); if (Dir.path.isAbsolute(path_str)) { // Absolute paths don't need changing. return path_str; } const child_cwd_rel: []const u8 = rel: { const child_lazy_cwd = run.cwd orelse break :rel path_str; - const child_cwd = child_lazy_cwd.getPath3(b, &run.step).toString(b.graph.arena) catch @panic("OOM"); + const child_cwd = child_lazy_cwd.getPath3(b, &run.step).toString(arena) catch @panic("OOM"); // Convert it from relative to *our* cwd, to relative to the *child's* cwd. - break :rel Dir.path.relative(b.graph.arena, child_cwd, path_str) catch @panic("OOM"); + break :rel Dir.path.relative(arena, graph.cache.cwd, &graph.env_map, child_cwd, path_str) catch @panic("OOM"); }; // Not every path can be made relative, e.g. if the path and the child cwd are on different // disk designators on Windows. In that case, `relative` will return an absolute path which we can // just return. - if (Dir.path.isAbsolute(child_cwd_rel)) { - return child_cwd_rel; - } + if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel; + // We're not done yet. In some cases this path must be prefixed with './': // * On POSIX, the executable name cannot be a single component like 'foo' // * Some executables might treat a leading '-' like a flag, which we must avoid // There's no harm in it, so just *always* apply this prefix. - return Dir.path.join(b.graph.arena, &.{ ".", child_cwd_rel }) catch @panic("OOM"); + return Dir.path.join(arena, &.{ ".", child_cwd_rel }) catch @panic("OOM"); } const IndexedOutput = struct { diff --git a/lib/std/Build/Watch/FsEvents.zig b/lib/std/Build/Watch/FsEvents.zig index 2a48534b3a..b23805c7ee 100644 --- a/lib/std/Build/Watch/FsEvents.zig +++ b/lib/std/Build/Watch/FsEvents.zig @@ -43,6 +43,8 @@ dispatch_queue: dispatch_queue_t, /// of writing. See the comment at the start of `wait` for details. since_event: FSEventStreamEventId, +cwd_path: []const u8, + /// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols /// is not present, `init` will close the framework and return an error. const ResolvedSymbols = struct { @@ -78,7 +80,7 @@ const ResolvedSymbols = struct { kCFAllocatorUseContext: *const CFAllocatorRef, }; -pub fn init() error{ OpenFrameworkFailed, MissingCoreServicesSymbol }!FsEvents { +pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreServicesSymbol }!FsEvents { var core_services = std.DynLib.open("/System/Library/Frameworks/CoreServices.framework/CoreServices") catch return error.OpenFrameworkFailed; errdefer core_services.close(); @@ -99,6 +101,7 @@ pub fn init() error{ OpenFrameworkFailed, MissingCoreServicesSymbol }!FsEvents { // Not `.since_now`, because this means we can init `FsEvents` *before* we do work in order // to notice any changes which happened during said work. .since_event = resolved_symbols.FSEventsGetCurrentEventId(), + .cwd_path = cwd_path, }; } @@ -120,9 +123,6 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step) defer fse.paths_arena = paths_arena_instance.state; const paths_arena = paths_arena_instance.allocator(); - const cwd_path = try std.process.getCwdAlloc(gpa); - defer gpa.free(cwd_path); - var need_dirs: std.StringArrayHashMapUnmanaged(void) = .empty; defer need_dirs.deinit(gpa); @@ -131,7 +131,9 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step) // We take `step` by pointer for a slight memory optimization in a moment. for (steps) |*step| { for (step.*.inputs.table.keys(), step.*.inputs.table.values()) |path, *files| { - const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{ cwd_path, path.root_dir.path orelse ".", path.sub_path }); + const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{ + fse.cwd_path, path.root_dir.path orelse ".", path.sub_path, + }); try need_dirs.put(gpa, resolved_dir, {}); for (files.items) |file_name| { const watch_path = if (std.mem.eql(u8, file_name, ".")) diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index ae9a200f24..9d1ad45524 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -482,8 +482,8 @@ pub fn serveFile( }); } pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void { - const gpa = ws.gpa; - const io = ws.graph.io; + const graph = ws.graph; + const io = graph.io; var send_buffer: [0x4000]u8 = undefined; var response = try request.respondStreaming(&send_buffer, .{ @@ -495,9 +495,6 @@ pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []cons }, }); - var cached_cwd_path: ?[]const u8 = null; - defer if (cached_cwd_path) |p| gpa.free(p); - var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer }; for (paths) |path| { @@ -516,10 +513,7 @@ pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []cons // resulting in modules named "" and "src". The compiler needs to tell the build system // about the module graph so that the build system can correctly encode this information in // the tar file. - archiver.prefix = path.root_dir.path orelse cwd: { - if (cached_cwd_path == null) cached_cwd_path = try std.process.getCwdAlloc(gpa); - break :cwd cached_cwd_path.?; - }; + archiver.prefix = path.root_dir.path orelse graph.cache.cwd; try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds())); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 08c12a5653..6843c02bb5 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -86,14 +86,14 @@ pub const Argv0 = switch (native_os) { const Environ = struct { /// Unmodified data directly from the OS. - block: process.Environ.Block = &.{}, + process_environ: process.Environ = .empty, /// Protected by `mutex`. Determines whether the other fields have been - /// memoized based on `block`. + /// memoized based on `process_environ`. initialized: bool = false, - /// Protected by `mutex`. Memoized based on `block`. Tracks whether the + /// Protected by `mutex`. Memoized based on `process_environ`. Tracks whether the /// environment variables are present, ignoring their value. exist: Exist = .{}, - /// Protected by `mutex`. Memoized based on `block`. + /// Protected by `mutex`. Memoized based on `process_environ`. string: String = .{}, /// ZIG_PROGRESS zig_progress_handle: std.Progress.ParentFileError!u31 = error.EnvironmentVariableMissing, @@ -1186,7 +1186,7 @@ pub fn init( .have_signal_handler = false, .argv0 = options.argv0, .worker_threads = .init(null), - .environ = .{ .block = options.environ.block }, + .environ = .{ .process_environ = options.environ }, .robust_cancel = options.robust_cancel, }; @@ -12693,7 +12693,7 @@ fn scanEnviron(t: *Threaded) void { comptime assert(@sizeOf(Environ.String) == 0); } } else { - for (t.environ.block) |opt_line| { + for (t.environ.process_environ.block) |opt_line| { const line = opt_line.?; var line_i: usize = 0; while (line[line_i] != 0 and line[line_i] != '=') : (line_i += 1) {} @@ -12837,7 +12837,7 @@ fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) proce .zig_progress_fd = prog_fd, })).ptr; } - break :m (try process.Environ.createBlockPosix(.{ .block = t.environ.block }, arena, .{ + break :m (try process.Environ.createBlockPosix(t.environ.process_environ, arena, .{ .zig_progress_fd = prog_fd, })).ptr; }; @@ -12934,6 +12934,7 @@ fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) proce return .{ .id = pid, + .thread_handle = {}, .stdin = switch (options.stdin) { .pipe => .{ .handle = stdin_pipe[1] }, else => null, diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig index a8465f52d4..4af6547249 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -13,16 +13,15 @@ //! https://github.com/WebAssembly/wasi-filesystem/issues/17#issuecomment-1430639353 const builtin = @import("builtin"); +const native_os = builtin.target.os.tag; + const std = @import("../std.zig"); -const debug = std.debug; -const assert = debug.assert; +const assert = std.debug.assert; const testing = std.testing; const mem = std.mem; -const ascii = std.ascii; -const Allocator = mem.Allocator; -const windows = std.os.windows; -const process = std.process; -const native_os = builtin.target.os.tag; +const Allocator = std.mem.Allocator; +const eqlIgnoreCaseWtf8 = std.os.windows.eqlIgnoreCaseWtf8; +const eqlIgnoreCaseWtf16 = std.os.windows.eqlIgnoreCaseWtf16; pub const sep_windows: u8 = '\\'; pub const sep_posix: u8 = '/'; @@ -281,7 +280,7 @@ pub fn isAbsolute(path: []const u8) bool { } fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool { - return switch (windows.getWin32PathType(T, path)) { + return switch (getWin32PathType(T, path)) { // Unambiguously absolute .drive_absolute, .unc_absolute, .local_device, .root_local_device => true, // Unambiguously relative @@ -515,13 +514,13 @@ test parsePathPosix { pub fn WindowsPath2(comptime T: type) type { return struct { - kind: windows.Win32PathType, + kind: Win32PathType, root: []const T, }; } pub fn parsePathWindows(comptime T: type, path: []const T) WindowsPath2(T) { - const kind = windows.getWin32PathType(T, path); + const kind = getWin32PathType(T, path); const root = root: switch (kind) { .drive_absolute, .drive_relative => { const drive_letter_len = getDriveLetter(T, path).len; @@ -731,7 +730,7 @@ fn parseUNC(comptime T: type, path: []const T) WindowsUNC(T) { // For the share, there can be any number of path separators between the server // and the share, so we want to skip over all of them instead of just looking for // the first one. - var it = std.mem.tokenizeAny(T, path[server_end + 1 ..], any_sep); + var it = mem.tokenizeAny(T, path[server_end + 1 ..], any_sep); const share = it.next() orelse return .{ .server = path[2..server_end], .sep_after_server = true, @@ -803,8 +802,8 @@ const DiskDesignatorKind = enum { drive, unc }; /// `p1` and `p2` are both assumed to be the `kind` provided. fn compareDiskDesignators(comptime T: type, kind: DiskDesignatorKind, p1: []const T, p2: []const T) bool { const eql = switch (T) { - u8 => windows.eqlIgnoreCaseWtf8, - u16 => windows.eqlIgnoreCaseWtf16, + u8 => eqlIgnoreCaseWtf8, + u16 => eqlIgnoreCaseWtf16, else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) is supported"), }; switch (kind) { @@ -1094,10 +1093,14 @@ pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator } /// This function is like a series of `cd` statements executed one after another. +/// /// It resolves "." and ".." to the best of its ability, but will not convert relative paths to /// an absolute path, use Io.Dir.realpath instead. +/// /// ".." components may persist in the resolved path if the resolved path is relative. +/// /// The result does not have a trailing path separator. +/// /// This function does not perform any syscalls. Executing this series of path /// lookups on the actual filesystem may produce different results due to /// symlinks. @@ -1494,25 +1497,54 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void { try testing.expectEqualSlices(u8, expected_output, basenameWindows(input)); } -pub const RelativeError = std.process.GetCwdAllocError; - -/// Returns the relative path from `from` to `to`. If `from` and `to` each -/// resolve to the same path (after calling `resolve` on each), a zero-length -/// string is returned. -/// On Windows, the result is not guaranteed to be relative, as the paths may be -/// on different volumes. In that case, the result will be the canonicalized absolute -/// path of `to`. -pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) RelativeError![]u8 { +/// Returns the non-absolute path from `from` to `to`. +/// +/// Other than memory allocation, this is a pure function; the result solely +/// depends on the input parameters. +/// +/// If `from` and `to` each resolve to the same path (after calling `resolve` +/// on each), a zero-length string is returned. +/// +/// See `relativePosix` and `relativeWindows` for operating system specific +/// details and for how `env_map` is used. +pub fn relative( + gpa: Allocator, + cwd: []const u8, + env_map: ?*const std.process.Environ.Map, + from: []const u8, + to: []const u8, +) Allocator.Error![]u8 { if (native_os == .windows) { - return relativeWindows(allocator, from, to); + return relativeWindows(gpa, cwd, env_map, from, to); } else { - return relativePosix(allocator, from, to); + return relativePosix(gpa, cwd, from, to); } } -pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 { - if (native_os != .windows) @compileError("this function relies on Windows-specific semantics"); - +/// Returns the non-absolute path from `from` to `to` according to Windows rules. +/// +/// Other than memory allocation, this is a pure function; the result solely +/// depends on the input parameters. +/// +/// If `from` and `to` each resolve to the same path (after calling `resolve` +/// on each), a zero-length string is returned. +/// +/// The result is not guaranteed to be relative, as the paths may be on +/// different volumes. In that case, the result will be the canonicalized +/// absolute path of `to`. +/// +/// Per-drive CWDs are stored in special semi-hidden environment variables of +/// the format `=:`, e.g. `=C:`. This type of CWD is purely a +/// shell concept, so there's no guarantee that it'll be set or that it'll even +/// be accurate. This is the only reason for the `env_map` parameter. `null` is +/// treated equivalent to the environment variable missing. +pub fn relativeWindows( + gpa: Allocator, + cwd: []const u8, + env_map: ?*const std.process.Environ.Map, + from: []const u8, + to: []const u8, +) Allocator.Error![]u8 { const parsed_from = parsePathWindows(u8, from); const parsed_to = parsePathWindows(u8, to); @@ -1533,14 +1565,14 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ! }; if (result_is_always_to) { - return windowsResolveAgainstCwd(allocator, to, parsed_to); + return windowsResolveAgainstCwd(gpa, cwd, env_map, to, parsed_to); } - const resolved_from = try windowsResolveAgainstCwd(allocator, from, parsed_from); - defer allocator.free(resolved_from); + const resolved_from = try windowsResolveAgainstCwd(gpa, cwd, env_map, from, parsed_from); + defer gpa.free(resolved_from); var clean_up_resolved_to = true; - const resolved_to = try windowsResolveAgainstCwd(allocator, to, parsed_to); - defer if (clean_up_resolved_to) allocator.free(resolved_to); + const resolved_to = try windowsResolveAgainstCwd(gpa, cwd, env_map, to, parsed_to); + defer if (clean_up_resolved_to) gpa.free(resolved_to); const parsed_resolved_from = parsePathWindows(u8, resolved_from); const parsed_resolved_to = parsePathWindows(u8, resolved_to); @@ -1569,18 +1601,18 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ! var from_it = mem.tokenizeAny(u8, resolved_from[parsed_resolved_from.root.len..], "/\\"); var to_it = mem.tokenizeAny(u8, resolved_to[parsed_resolved_to.root.len..], "/\\"); while (true) { - const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest()); + const from_component = from_it.next() orelse return gpa.dupe(u8, to_it.rest()); const to_rest = to_it.rest(); if (to_it.next()) |to_component| { - if (windows.eqlIgnoreCaseWtf8(from_component, to_component)) + if (eqlIgnoreCaseWtf8(from_component, to_component)) continue; } var up_index_end = "..".len; while (from_it.next()) |_| { up_index_end += "\\..".len; } - const result = try allocator.alloc(u8, up_index_end + @intFromBool(to_rest.len > 0) + to_rest.len); - errdefer allocator.free(result); + const result = try gpa.alloc(u8, up_index_end + @intFromBool(to_rest.len > 0) + to_rest.len); + errdefer gpa.free(result); result[0..2].* = "..".*; var result_index: usize = 2; @@ -1597,85 +1629,60 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ! result_index += to_component.len; } - return allocator.realloc(result, result_index); + return gpa.realloc(result, result_index); } return [_]u8{}; } -fn windowsResolveAgainstCwd(allocator: Allocator, path: []const u8, parsed: WindowsPath2(u8)) ![]u8 { +fn windowsResolveAgainstCwd( + gpa: Allocator, + cwd: []const u8, + env_map: ?*const std.process.Environ.Map, + path: []const u8, + parsed: WindowsPath2(u8), +) ![]u8 { // Space for 256 WTF-16 code units; potentially 3 WTF-8 bytes per WTF-16 code unit - var temp_allocator_state = std.heap.stackFallback(256 * 3, allocator); + var temp_allocator_state = std.heap.stackFallback(256 * 3, gpa); return switch (parsed.kind) { .drive_absolute, .unc_absolute, .root_local_device, .local_device, - => try resolveWindows(allocator, &.{path}), - .relative => blk: { - const temp_allocator = temp_allocator_state.get(); + => try resolveWindows(gpa, &.{path}), - const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath; - const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2]; + .relative => try resolveWindows(gpa, &.{ cwd, path }), - const wtf8_len = std.unicode.calcWtf8Len(cwd_w); - const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len); - defer temp_allocator.free(wtf8_buf); - assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len); - - break :blk try resolveWindows(allocator, &.{ wtf8_buf, path }); - }, .rooted => blk: { - const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath; - const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2]; - const parsed_cwd = parsePathWindows(u16, cwd_w); + const parsed_cwd = parsePathWindows(u8, cwd); switch (parsed_cwd.kind) { .drive_absolute => { var drive_buf = "_:\\".*; - drive_buf[0] = @truncate(cwd_w[0]); - break :blk try resolveWindows(allocator, &.{ &drive_buf, path }); + drive_buf[0] = cwd[0]; + break :blk try resolveWindows(gpa, &.{ &drive_buf, path }); }, .unc_absolute => { - const temp_allocator = temp_allocator_state.get(); - var root_buf = try temp_allocator.alloc(u8, parsed_cwd.root.len * 3); - defer temp_allocator.free(root_buf); - - const wtf8_len = std.unicode.wtf16LeToWtf8(root_buf, parsed_cwd.root); - const root = root_buf[0..wtf8_len]; - break :blk try resolveWindows(allocator, &.{ root, path }); + break :blk try resolveWindows(gpa, &.{ parsed_cwd.root, path }); }, // Effectively a malformed CWD, give up and just return a normalized path - else => break :blk try resolveWindows(allocator, &.{path}), + else => break :blk try resolveWindows(gpa, &.{path}), } }, .drive_relative => blk: { const temp_allocator = temp_allocator_state.get(); const drive_cwd = drive_cwd: { - const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath; - const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2]; - const parsed_cwd = parsePathWindows(u16, cwd_w); + const parsed_cwd = parsePathWindows(u8, cwd); if (parsed_cwd.kind == .drive_absolute) { const drive_letter_w = parsed_cwd.root[0]; const drive_letters_match = drive_letter_w <= 0x7F and - ascii.toUpper(@intCast(drive_letter_w)) == ascii.toUpper(parsed.root[0]); - if (drive_letters_match) { - const wtf8_len = std.unicode.calcWtf8Len(cwd_w); - const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len); - assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len); - break :drive_cwd wtf8_buf[0..]; - } + std.ascii.toUpper(@intCast(drive_letter_w)) == std.ascii.toUpper(parsed.root[0]); + if (drive_letters_match) + break :drive_cwd cwd; - // Per-drive CWD's are stored in special semi-hidden environment variables - // of the format `=:`, e.g. `=C:`. This type of CWD is - // purely a shell concept, so there's no guarantee that it'll be set - // or that it'll even be accurate. - var key_buf = std.unicode.wtf8ToWtf16LeStringLiteral("=_:").*; - key_buf[1] = parsed.root[0]; - if (std.process.getenvW(&key_buf)) |drive_cwd_w| { - const wtf8_len = std.unicode.calcWtf8Len(drive_cwd_w); - const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len); - assert(std.unicode.wtf16LeToWtf8(wtf8_buf, drive_cwd_w) == wtf8_len); - break :drive_cwd wtf8_buf[0..]; + if (env_map) |m| { + if (m.get(&.{ '=', parsed.root[0], ':' })) |v| { + break :drive_cwd try temp_allocator.dupe(u8, v); + } } } @@ -1686,16 +1693,20 @@ fn windowsResolveAgainstCwd(allocator: Allocator, path: []const u8, parsed: Wind break :drive_cwd drive_buf; }; defer temp_allocator.free(drive_cwd); - break :blk try resolveWindows(allocator, &.{ drive_cwd, path }); + break :blk try resolveWindows(gpa, &.{ drive_cwd, path }); }, }; } -pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 { - if (native_os == .windows) @compileError("this function relies on semantics that do not apply to Windows"); - - const cwd = try process.getCwdAlloc(allocator); - defer allocator.free(cwd); +/// Returns the non-absolute path from `from` to `to` according to Windows rules. +/// +/// Other than memory allocation, this is a pure function; the result solely +/// depends on the input parameters. +/// +/// If `from` and `to` each resolve to the same path (after calling `resolve` +/// on each), a zero-length string is returned. +/// +pub fn relativePosix(allocator: Allocator, cwd: []const u8, from: []const u8, to: []const u8) Allocator.Error![]u8 { const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from }); defer allocator.free(resolved_from); const resolved_to = try resolvePosix(allocator, &[_][]const u8{ cwd, to }); @@ -1736,69 +1747,67 @@ pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![] } test relative { - if (native_os == .windows) { - try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games"); - try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", ".."); - try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"); - try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", ""); - try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"); - try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc"); - try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"); - try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\"); - try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", ""); - try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"); - try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."); - try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json"); - try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"); - try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"); - try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"); - try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."); - try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"); - try testRelativeWindows("\\\\foo/bar\\baz-quux", "//foo\\bar/baz", "..\\baz"); - try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"); - try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz"); - try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux"); - try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz"); - try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "\\\\foo\\baz-quux"); - try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"); - try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"); + try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games"); + try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", ".."); + try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"); + try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", ""); + try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"); + try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc"); + try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"); + try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\"); + try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", ""); + try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"); + try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."); + try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json"); + try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"); + try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"); + try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"); + try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."); + try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"); + try testRelativeWindows("\\\\foo/bar\\baz-quux", "//foo\\bar/baz", "..\\baz"); + try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"); + try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz"); + try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux"); + try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz"); + try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "\\\\foo\\baz-quux"); + try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"); + try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"); - try testRelativeWindows("c:blah\\blah", "c:foo", "..\\..\\foo"); - try testRelativeWindows("c:foo", "c:foo\\bar", "bar"); - try testRelativeWindows("\\blah\\blah", "\\foo", "..\\..\\foo"); - try testRelativeWindows("\\foo", "\\foo\\bar", "bar"); + try testRelativeWindows("c:blah\\blah", "c:foo", "..\\..\\foo"); + try testRelativeWindows("c:foo", "c:foo\\bar", "bar"); + try testRelativeWindows("\\blah\\blah", "\\foo", "..\\..\\foo"); + try testRelativeWindows("\\foo", "\\foo\\bar", "bar"); - try testRelativeWindows("a/b/c", "a\\b", ".."); - try testRelativeWindows("a/b/c", "a", "..\\.."); - try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d"); + try testRelativeWindows("a/b/c", "a\\b", ".."); + try testRelativeWindows("a/b/c", "a", "..\\.."); + try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d"); - try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", ""); - // Unicode-aware case-insensitive path comparison - try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", ""); - } else { - try testRelativePosix("/var/lib", "/var", ".."); - try testRelativePosix("/var/lib", "/bin", "../../bin"); - try testRelativePosix("/var/lib", "/var/lib", ""); - try testRelativePosix("/var/lib", "/var/apache", "../apache"); - try testRelativePosix("/var/", "/var/lib", "lib"); - try testRelativePosix("/", "/var/lib", "var/lib"); - try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json"); - try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."); - try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz"); - try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"); - try testRelativePosix("/baz-quux", "/baz", "../baz"); - try testRelativePosix("/baz", "/baz-quux", "../baz-quux"); - } + try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", ""); + // Unicode-aware case-insensitive path comparison + try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", ""); + + try testRelativePosix("/var/lib", "/var", ".."); + try testRelativePosix("/var/lib", "/bin", "../../bin"); + try testRelativePosix("/var/lib", "/var/lib", ""); + try testRelativePosix("/var/lib", "/var/apache", "../apache"); + try testRelativePosix("/var/", "/var/lib", "lib"); + try testRelativePosix("/", "/var/lib", "var/lib"); + try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json"); + try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."); + try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz"); + try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"); + try testRelativePosix("/baz-quux", "/baz", "../baz"); + try testRelativePosix("/baz", "/baz-quux", "../baz-quux"); } fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void { - const result = try relativePosix(testing.allocator, from, to); + const result = try relativePosix(testing.allocator, ".", from, to); defer testing.allocator.free(result); try testing.expectEqualStrings(expected_output, result); } fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) !void { - const result = try relativeWindows(testing.allocator, from, to); + const result = try relativeWindows(testing.allocator, ".", null, from, to); defer testing.allocator.free(result); try testing.expectEqualStrings(expected_output, result); } @@ -2554,3 +2563,124 @@ pub const fmtAsUtf8Lossy = std.unicode.fmtUtf8; /// a lossy conversion if the path contains any unpaired surrogates. /// Unpaired surrogates are replaced by the replacement character (U+FFFD). pub const fmtWtf16LeAsUtf8Lossy = std.unicode.fmtUtf16Le; + +/// Similar to `RTL_PATH_TYPE`, but without the `UNKNOWN` path type. +pub const Win32PathType = enum { + /// `\\server\share\foo` + unc_absolute, + /// `C:\foo` + drive_absolute, + /// `C:foo` + drive_relative, + /// `\foo` + rooted, + /// `foo` + relative, + /// `\\.\foo`, `\\?\foo` + local_device, + /// `\\.`, `\\?` + root_local_device, +}; + +/// Get the path type of a Win32 namespace path. +/// Similar to `RtlDetermineDosPathNameType_U`. +/// If `T` is `u16`, then `path` should be encoded as WTF-16LE. +pub fn getWin32PathType(comptime T: type, path: []const T) Win32PathType { + if (path.len < 1) return .relative; + + const windows_path = std.fs.path.PathType.windows; + if (windows_path.isSep(T, path[0])) { + // \x + if (path.len < 2 or !windows_path.isSep(T, path[1])) return .rooted; + // \\. or \\? + if (path.len > 2 and (path[2] == mem.nativeToLittle(T, '.') or path[2] == mem.nativeToLittle(T, '?'))) { + // exactly \\. or \\? with nothing trailing + if (path.len == 3) return .root_local_device; + // \\.\x or \\?\x + if (windows_path.isSep(T, path[3])) return .local_device; + } + // \\x + return .unc_absolute; + } else { + // Some choice has to be made about how non-ASCII code points as drive-letters are handled, since + // path[0] is a different size for WTF-16 vs WTF-8, leading to a potential mismatch in classification + // for a WTF-8 path and its WTF-16 equivalent. For example, `€:\` encoded in WTF-16 is three code + // units `<0x20AC>:\` whereas `€:\` encoded as WTF-8 is 6 code units `<0xE2><0x82><0xAC>:\` so + // checking path[0], path[1] and path[2] would not behave the same between WTF-8/WTF-16. + // + // `RtlDetermineDosPathNameType_U` exclusively deals with WTF-16 and considers + // `€:\` a drive-absolute path, but code points that take two WTF-16 code units to encode get + // classified as a relative path (e.g. with U+20000 as the drive-letter that'd be encoded + // in WTF-16 as `<0xD840><0xDC00>:\` and be considered a relative path). + // + // The choice made here is to emulate the behavior of `RtlDetermineDosPathNameType_U` for both + // WTF-16 and WTF-8. This is because, while unlikely and not supported by the Disk Manager GUI, + // drive letters are not actually restricted to A-Z. Using `SetVolumeMountPointW` will allow you + // to set any byte value as a drive letter, and going through `IOCTL_MOUNTMGR_CREATE_POINT` will + // allow you to set any WTF-16 code unit as a drive letter. + // + // Non-A-Z drive letters don't interact well with most of Windows, but certain things do work, e.g. + // `cd /D €:\` will work, filesystem functions still work, etc. + // + // The unfortunate part of this is that this makes handling WTF-8 more complicated as we can't + // just check path[0], path[1], path[2]. + const colon_i: usize = switch (T) { + u8 => i: { + const code_point_len = std.unicode.utf8ByteSequenceLength(path[0]) catch return .relative; + // Conveniently, 4-byte sequences in WTF-8 have the same starting code point + // as 2-code-unit sequences in WTF-16. + if (code_point_len > 3) return .relative; + break :i code_point_len; + }, + u16 => 1, + else => @compileError("unsupported type: " ++ @typeName(T)), + }; + // x + if (path.len < colon_i + 1 or path[colon_i] != mem.nativeToLittle(T, ':')) return .relative; + // x:\ + if (path.len > colon_i + 1 and windows_path.isSep(T, path[colon_i + 1])) return .drive_absolute; + // x: + return .drive_relative; + } +} + +test getWin32PathType { + try std.testing.expectEqual(.relative, getWin32PathType(u8, "")); + try std.testing.expectEqual(.relative, getWin32PathType(u8, "x")); + try std.testing.expectEqual(.relative, getWin32PathType(u8, "x\\")); + + try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "//.")); + try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "/\\?")); + try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "\\\\?")); + + try std.testing.expectEqual(.local_device, getWin32PathType(u8, "//./x")); + try std.testing.expectEqual(.local_device, getWin32PathType(u8, "/\\?\\x")); + try std.testing.expectEqual(.local_device, getWin32PathType(u8, "\\\\?\\x")); + // local device paths require a path separator after the root, otherwise it is considered a UNC path + try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\?x")); + try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//.x")); + + try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//")); + try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\x")); + try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//x")); + + try std.testing.expectEqual(.rooted, getWin32PathType(u8, "\\x")); + try std.testing.expectEqual(.rooted, getWin32PathType(u8, "/")); + + try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:")); + try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:abc")); + try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:a/b/c")); + + try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\")); + try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\abc")); + try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:/a/b/c")); + + // Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter + try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "€:\\")); + try std.testing.expectEqual(.drive_absolute, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:\\"))); + try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "€:")); + try std.testing.expectEqual(.drive_relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:"))); + // But code points that are encoded as two WTF-16 code units are not + try std.testing.expectEqual(.relative, getWin32PathType(u8, "\u{10000}:\\")); + try std.testing.expectEqual(.relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("\u{10000}:\\"))); +} diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index b47533a3ec..f5bc595fde 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -2927,7 +2927,7 @@ pub fn CreateSymbolicLink( // Already an NT path, no need to do anything to it break :target_path target_path; } else { - switch (getWin32PathType(u16, target_path)) { + switch (std.fs.path.getWin32PathType(u16, target_path)) { // Rooted paths need to avoid getting put through wToPrefixedFileW // (and they are treated as relative in this context) // Note: It seems that rooted paths in symbolic links are relative to @@ -4235,7 +4235,7 @@ pub const RemoveDotDirsError = error{TooManyParentDirs}; /// 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] != '\\'); + assert(path.len == 0 or path[0] != '\\'); var write_idx: usize = 0; var read_idx: usize = 0; @@ -4251,7 +4251,7 @@ pub fn removeDotDirsSanitized(comptime T: type, path: []T) RemoveDotDirsError!us } 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); + assert(write_idx >= 2); write_idx -= 1; while (true) { write_idx -= 1; @@ -4353,7 +4353,7 @@ pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWE path_space.data[path_space.len] = 0; return path_space; } else { - const path_type = getWin32PathType(u16, path); + const path_type = std.fs.path.getWin32PathType(u16, path); var path_space: PathSpace = undefined; if (path_type == .local_device) { switch (getLocalDevicePathType(u16, path)) { @@ -4491,8 +4491,8 @@ pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWE if (path_type == .unc_absolute) { // Now add in the UNC, the `C` should overwrite the first `\` of the // FullPathName, ultimately resulting in `\??\UNC\` - std.debug.assert(path_space.data[path_buf_offset] == '\\'); - std.debug.assert(path_space.data[path_buf_offset + 1] == '\\'); + assert(path_space.data[path_buf_offset] == '\\'); + assert(path_space.data[path_buf_offset + 1] == '\\'); const unc = [_]u16{ 'U', 'N', 'C' }; path_space.data[nt_prefix.len..][0..unc.len].* = unc; } @@ -4500,127 +4500,6 @@ pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWE } } -/// Similar to `RTL_PATH_TYPE`, but without the `UNKNOWN` path type. -pub const Win32PathType = enum { - /// `\\server\share\foo` - unc_absolute, - /// `C:\foo` - drive_absolute, - /// `C:foo` - drive_relative, - /// `\foo` - rooted, - /// `foo` - relative, - /// `\\.\foo`, `\\?\foo` - local_device, - /// `\\.`, `\\?` - root_local_device, -}; - -/// Get the path type of a Win32 namespace path. -/// Similar to `RtlDetermineDosPathNameType_U`. -/// If `T` is `u16`, then `path` should be encoded as WTF-16LE. -pub fn getWin32PathType(comptime T: type, path: []const T) Win32PathType { - if (path.len < 1) return .relative; - - const windows_path = std.fs.path.PathType.windows; - if (windows_path.isSep(T, path[0])) { - // \x - if (path.len < 2 or !windows_path.isSep(T, path[1])) return .rooted; - // \\. or \\? - if (path.len > 2 and (path[2] == mem.nativeToLittle(T, '.') or path[2] == mem.nativeToLittle(T, '?'))) { - // exactly \\. or \\? with nothing trailing - if (path.len == 3) return .root_local_device; - // \\.\x or \\?\x - if (windows_path.isSep(T, path[3])) return .local_device; - } - // \\x - return .unc_absolute; - } else { - // Some choice has to be made about how non-ASCII code points as drive-letters are handled, since - // path[0] is a different size for WTF-16 vs WTF-8, leading to a potential mismatch in classification - // for a WTF-8 path and its WTF-16 equivalent. For example, `€:\` encoded in WTF-16 is three code - // units `<0x20AC>:\` whereas `€:\` encoded as WTF-8 is 6 code units `<0xE2><0x82><0xAC>:\` so - // checking path[0], path[1] and path[2] would not behave the same between WTF-8/WTF-16. - // - // `RtlDetermineDosPathNameType_U` exclusively deals with WTF-16 and considers - // `€:\` a drive-absolute path, but code points that take two WTF-16 code units to encode get - // classified as a relative path (e.g. with U+20000 as the drive-letter that'd be encoded - // in WTF-16 as `<0xD840><0xDC00>:\` and be considered a relative path). - // - // The choice made here is to emulate the behavior of `RtlDetermineDosPathNameType_U` for both - // WTF-16 and WTF-8. This is because, while unlikely and not supported by the Disk Manager GUI, - // drive letters are not actually restricted to A-Z. Using `SetVolumeMountPointW` will allow you - // to set any byte value as a drive letter, and going through `IOCTL_MOUNTMGR_CREATE_POINT` will - // allow you to set any WTF-16 code unit as a drive letter. - // - // Non-A-Z drive letters don't interact well with most of Windows, but certain things do work, e.g. - // `cd /D €:\` will work, filesystem functions still work, etc. - // - // The unfortunate part of this is that this makes handling WTF-8 more complicated as we can't - // just check path[0], path[1], path[2]. - const colon_i: usize = switch (T) { - u8 => i: { - const code_point_len = std.unicode.utf8ByteSequenceLength(path[0]) catch return .relative; - // Conveniently, 4-byte sequences in WTF-8 have the same starting code point - // as 2-code-unit sequences in WTF-16. - if (code_point_len > 3) return .relative; - break :i code_point_len; - }, - u16 => 1, - else => @compileError("unsupported type: " ++ @typeName(T)), - }; - // x - if (path.len < colon_i + 1 or path[colon_i] != mem.nativeToLittle(T, ':')) return .relative; - // x:\ - if (path.len > colon_i + 1 and windows_path.isSep(T, path[colon_i + 1])) return .drive_absolute; - // x: - return .drive_relative; - } -} - -test getWin32PathType { - try std.testing.expectEqual(.relative, getWin32PathType(u8, "")); - try std.testing.expectEqual(.relative, getWin32PathType(u8, "x")); - try std.testing.expectEqual(.relative, getWin32PathType(u8, "x\\")); - - try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "//.")); - try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "/\\?")); - try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "\\\\?")); - - try std.testing.expectEqual(.local_device, getWin32PathType(u8, "//./x")); - try std.testing.expectEqual(.local_device, getWin32PathType(u8, "/\\?\\x")); - try std.testing.expectEqual(.local_device, getWin32PathType(u8, "\\\\?\\x")); - // local device paths require a path separator after the root, otherwise it is considered a UNC path - try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\?x")); - try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//.x")); - - try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//")); - try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\x")); - try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//x")); - - try std.testing.expectEqual(.rooted, getWin32PathType(u8, "\\x")); - try std.testing.expectEqual(.rooted, getWin32PathType(u8, "/")); - - try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:")); - try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:abc")); - try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:a/b/c")); - - try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\")); - try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\abc")); - try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:/a/b/c")); - - // Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter - try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "€:\\")); - try std.testing.expectEqual(.drive_absolute, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:\\"))); - try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "€:")); - try std.testing.expectEqual(.drive_relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:"))); - // But code points that are encoded as two WTF-16 code units are not - try std.testing.expectEqual(.relative, getWin32PathType(u8, "\u{10000}:\\")); - try std.testing.expectEqual(.relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("\u{10000}:\\"))); -} - /// Returns true if the path starts with `\??\`, which is indicative of an NT path /// but is not enough to fully distinguish between NT paths and Win32 paths, as /// `\??\` is not actually a distinct prefix but rather the path to a special virtual @@ -4663,7 +4542,7 @@ const LocalDevicePathType = enum { /// Asserts `path` is of type `Win32PathType.local_device`. fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType { if (std.debug.runtime_safety) { - std.debug.assert(getWin32PathType(T, path) == .local_device); + assert(std.fs.path.getWin32PathType(T, path) == .local_device); } const backslash = mem.nativeToLittle(T, '\\'); diff --git a/lib/std/process/Args.zig b/lib/std/process/Args.zig index 4cf9c438b2..e10764ceb5 100644 --- a/lib/std/process/Args.zig +++ b/lib/std/process/Args.zig @@ -12,6 +12,7 @@ vector: Vector, pub const Vector = switch (native_os) { .windows => []const u16, // WTF-16 encoded + .freestanding, .other => void, else => []const [*:0]const u8, }; @@ -57,7 +58,7 @@ pub const Iterator = struct { /// Returned slice is pointing to the iterator's internal buffer. /// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On other platforms, the result is an opaque sequence of bytes with no particular encoding. - pub fn next(it: *Iterator) ?([:0]const u8) { + pub fn next(it: *Iterator) ?[:0]const u8 { return it.inner.next(); } diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 176405cc8b..17e15f208d 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -21,7 +21,7 @@ pub const Id = switch (native_os) { /// On Windows this is the hProcess. /// On POSIX this is the pid. id: ?Id, -thread_handle: if (native_os == .windows) std.os.windows.HANDLE else void = {}, +thread_handle: if (native_os == .windows) std.os.windows.HANDLE else void, /// The writing end of the child process's standard input pipe. /// Usage requires `process.SpawnOptions.StdIo.pipe`. stdin: ?File, diff --git a/lib/std/process/Environ.zig b/lib/std/process/Environ.zig index 2556ae558e..95446aae25 100644 --- a/lib/std/process/Environ.zig +++ b/lib/std/process/Environ.zig @@ -12,11 +12,6 @@ const posix = std.posix; const mem = std.mem; /// Unmodified, unprocessed data provided by the operating system. -/// -/// On Windows this might point to memory in the PEB. -/// -/// On WASI without libc, this is void because the environment has to be -/// queried and heap-allocated at runtime. block: Block, pub const empty: Environ = .{ @@ -26,12 +21,19 @@ pub const empty: Environ = .{ }, }; +/// On WASI without libc, this is `void` because the environment has to be +/// queried and heap-allocated at runtime. +/// +/// On Windows, the memory pointed at by the PEB changes when the environment +/// is modified, so a long-lived pointer cannot be used. Therefore, on this +/// operating system `void` is also used. pub const Block = switch (native_os) { - .windows => [*:0]const u16, + .windows => void, .wasi => switch (builtin.link_libc) { false => void, true => [:null]const ?[*:0]const u8, }, + .freestanding, .other => void, else => [:null]const ?[*:0]const u8, }; @@ -345,7 +347,7 @@ pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map { errdefer result.deinit(); if (native_os == .windows) { - const ptr = env.block; + const ptr = std.os.windows.peb().ProcessParameters.Environment; var i: usize = 0; while (ptr[i] != 0) { diff --git a/lib/std/start.zig b/lib/std/start.zig index df4d6e9aff..6db64c26bd 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -11,118 +11,63 @@ const native_os = builtin.os.tag; const start_sym_name = if (native_arch.isMIPS()) "__start" else "_start"; -// The self-hosted compiler is not fully capable of handling all of this start.zig file. -// Until then, we have simplified logic here for self-hosted. TODO remove this once -// self-hosted is capable enough to handle all of the real start.zig logic. -pub const simplified_logic = switch (builtin.zig_backend) { - .stage2_aarch64, - .stage2_arm, - .stage2_powerpc, - .stage2_sparc64, - .stage2_spirv, - .stage2_x86, - => true, - else => false, -}; - comptime { // No matter what, we import the root file, so that any export, test, comptime // decls there get run. _ = root; - if (simplified_logic) { - if (builtin.output_mode == .Exe) { - if ((builtin.link_libc or builtin.object_format == .c) and @hasDecl(root, "main")) { - if (!@typeInfo(@TypeOf(root.main)).@"fn".calling_convention.eql(.c)) { - @export(&main2, .{ .name = "main" }); - } - } else if (builtin.os.tag == .windows) { - if (!@hasDecl(root, "wWinMainCRTStartup") and !@hasDecl(root, "mainCRTStartup")) { - @export(&wWinMainCRTStartup2, .{ .name = "wWinMainCRTStartup" }); - } - } else if (builtin.os.tag == .opencl or builtin.os.tag == .vulkan) { - if (@hasDecl(root, "main")) - @export(&spirvMain2, .{ .name = "main" }); - } else { - if (!@hasDecl(root, "_start")) { - @export(&_start2, .{ .name = "_start" }); - } - } + if (builtin.output_mode == .Lib and builtin.link_mode == .dynamic) { + if (native_os == .windows and !@hasDecl(root, "_DllMainCRTStartup")) { + @export(&_DllMainCRTStartup, .{ .name = "_DllMainCRTStartup" }); } - } else { - if (builtin.output_mode == .Lib and builtin.link_mode == .dynamic) { - if (native_os == .windows and !@hasDecl(root, "_DllMainCRTStartup")) { - @export(&_DllMainCRTStartup, .{ .name = "_DllMainCRTStartup" }); + } else if (builtin.output_mode == .Exe or @hasDecl(root, "main")) { + if (builtin.link_libc and @hasDecl(root, "main")) { + if (native_arch.isWasm()) { + @export(&mainWithoutEnv, .{ .name = "__main_argc_argv" }); + } else if (!@typeInfo(@TypeOf(root.main)).@"fn".calling_convention.eql(.c)) { + @export(&main, .{ .name = "main" }); } - } else if (builtin.output_mode == .Exe or @hasDecl(root, "main")) { - if (builtin.link_libc and @hasDecl(root, "main")) { - if (native_arch.isWasm()) { - @export(&mainWithoutEnv, .{ .name = "__main_argc_argv" }); - } else if (!@typeInfo(@TypeOf(root.main)).@"fn".calling_convention.eql(.c)) { - @export(&main, .{ .name = "main" }); - } - } else if (native_os == .windows and builtin.link_libc and @hasDecl(root, "wWinMain")) { - if (!@typeInfo(@TypeOf(root.wWinMain)).@"fn".calling_convention.eql(.c)) { - @export(&wWinMain, .{ .name = "wWinMain" }); - } - } else if (native_os == .windows) { - if (!@hasDecl(root, "WinMain") and !@hasDecl(root, "WinMainCRTStartup") and - !@hasDecl(root, "wWinMain") and !@hasDecl(root, "wWinMainCRTStartup")) - { - @export(&WinStartup, .{ .name = "wWinMainCRTStartup" }); - } else if (@hasDecl(root, "WinMain") and !@hasDecl(root, "WinMainCRTStartup") and - !@hasDecl(root, "wWinMain") and !@hasDecl(root, "wWinMainCRTStartup")) - { - @compileError("WinMain not supported; declare wWinMain or main instead"); - } else if (@hasDecl(root, "wWinMain") and !@hasDecl(root, "wWinMainCRTStartup") and - !@hasDecl(root, "WinMain") and !@hasDecl(root, "WinMainCRTStartup")) - { - @export(&wWinMainCRTStartup, .{ .name = "wWinMainCRTStartup" }); - } - } else if (native_os == .uefi) { - if (!@hasDecl(root, "EfiMain")) @export(&EfiMain, .{ .name = "EfiMain" }); - } else if (native_os == .wasi) { - const wasm_start_sym = switch (builtin.wasi_exec_model) { - .reactor => "_initialize", - .command => "_start", - }; - if (!@hasDecl(root, wasm_start_sym) and @hasDecl(root, "main")) { - // Only call main when defined. For WebAssembly it's allowed to pass `-fno-entry` in which - // case it's not required to provide an entrypoint such as main. - @export(&wasi_start, .{ .name = wasm_start_sym }); - } - } else if (native_arch.isWasm() and native_os == .freestanding) { + } else if (native_os == .windows and builtin.link_libc and @hasDecl(root, "wWinMain")) { + if (!@typeInfo(@TypeOf(root.wWinMain)).@"fn".calling_convention.eql(.c)) { + @export(&wWinMain, .{ .name = "wWinMain" }); + } + } else if (native_os == .windows) { + if (!@hasDecl(root, "WinMain") and !@hasDecl(root, "WinMainCRTStartup") and + !@hasDecl(root, "wWinMain") and !@hasDecl(root, "wWinMainCRTStartup")) + { + @export(&WinStartup, .{ .name = "wWinMainCRTStartup" }); + } else if (@hasDecl(root, "WinMain") and !@hasDecl(root, "WinMainCRTStartup") and + !@hasDecl(root, "wWinMain") and !@hasDecl(root, "wWinMainCRTStartup")) + { + @compileError("WinMain not supported; declare wWinMain or main instead"); + } else if (@hasDecl(root, "wWinMain") and !@hasDecl(root, "wWinMainCRTStartup") and + !@hasDecl(root, "WinMain") and !@hasDecl(root, "WinMainCRTStartup")) + { + @export(&wWinMainCRTStartup, .{ .name = "wWinMainCRTStartup" }); + } + } else if (native_os == .uefi) { + if (!@hasDecl(root, "EfiMain")) @export(&EfiMain, .{ .name = "EfiMain" }); + } else if (native_os == .wasi) { + const wasm_start_sym = switch (builtin.wasi_exec_model) { + .reactor => "_initialize", + .command => "_start", + }; + if (!@hasDecl(root, wasm_start_sym) and @hasDecl(root, "main")) { // Only call main when defined. For WebAssembly it's allowed to pass `-fno-entry` in which // case it's not required to provide an entrypoint such as main. - if (!@hasDecl(root, start_sym_name) and @hasDecl(root, "main")) @export(&wasm_freestanding_start, .{ .name = start_sym_name }); - } else switch (native_os) { - .other, .freestanding, .@"3ds", .vita => {}, - else => if (!@hasDecl(root, start_sym_name)) @export(&_start, .{ .name = start_sym_name }), + @export(&wasi_start, .{ .name = wasm_start_sym }); } + } else if (native_arch.isWasm() and native_os == .freestanding) { + // Only call main when defined. For WebAssembly it's allowed to pass `-fno-entry` in which + // case it's not required to provide an entrypoint such as main. + if (!@hasDecl(root, start_sym_name) and @hasDecl(root, "main")) @export(&wasm_freestanding_start, .{ .name = start_sym_name }); + } else switch (native_os) { + .other, .freestanding, .@"3ds", .vita => {}, + else => if (!@hasDecl(root, start_sym_name)) @export(&_start, .{ .name = start_sym_name }), } } } -// Simplified start code for stage2 until it supports more language features /// - -fn main2() callconv(.c) c_int { - return callMain(); -} - -fn _start2() callconv(.withStackAlign(.c, 1)) noreturn { - std.process.exit(callMain()); -} - -fn spirvMain2() callconv(.kernel) void { - root.main(); -} - -fn wWinMainCRTStartup2() callconv(.c) noreturn { - std.process.exit(callMain()); -} - -//////////////////////////////////////////////////////////////////////////////// - fn _DllMainCRTStartup( hinstDLL: std.os.windows.HINSTANCE, fdwReason: std.os.windows.DWORD, @@ -142,15 +87,15 @@ fn _DllMainCRTStartup( fn wasm_freestanding_start() callconv(.c) void { // This is marked inline because for some reason LLVM in // release mode fails to inline it, and we want fewer call frames in stack traces. - _ = @call(.always_inline, callMain, .{}); + _ = @call(.always_inline, callMain, .{ {}, {} }); } fn wasi_start() callconv(.c) void { // The function call is marked inline because for some reason LLVM in // release mode fails to inline it, and we want fewer call frames in stack traces. switch (builtin.wasi_exec_model) { - .reactor => _ = @call(.always_inline, callMain, .{}), - .command => std.os.wasi.proc_exit(@call(.always_inline, callMain, .{})), + .reactor => _ = @call(.always_inline, callMain, .{ {}, {} }), + .command => std.os.wasi.proc_exit(@call(.always_inline, callMain, .{ {}, {} })), } } @@ -524,13 +469,10 @@ fn WinStartup() callconv(.withStackAlign(.c, 1)) noreturn { std.debug.maybeEnableSegfaultHandler(); - const peb = std.os.windows.peb(); const cmd_line = std.os.windows.peb().ProcessParameters.CommandLine; + const cmd_line_w = cmd_line.Buffer.?[0..@divExact(cmd_line.Length, 2)]; - std.os.windows.ntdll.RtlExitUserProcess(callMain( - cmd_line.Buffer.?[0..@divExact(cmd_line.Length, 2)], - peb.ProcessParameters.Environment, - )); + std.os.windows.ntdll.RtlExitUserProcess(callMain(cmd_line_w, {})); } fn wWinMainCRTStartup() callconv(.withStackAlign(.c, 1)) noreturn { @@ -637,6 +579,7 @@ fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn { } fn expandStackSize(phdrs: []elf.Phdr) void { + @disableInstrumentation(); for (phdrs) |*phdr| { switch (phdr.p_type) { elf.PT_GNU_STACK => { @@ -674,7 +617,7 @@ fn expandStackSize(phdrs: []elf.Phdr) void { inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [:null]?[*:0]u8) u8 { if (std.Options.debug_threaded_io) |t| { if (@sizeOf(std.Io.Threaded.Argv0) != 0) t.argv0.value = argv[0]; - t.environ = .{ .block = envp }; + t.environ = .{ .process_environ = .{ .block = envp } }; } std.debug.maybeEnableSegfaultHandler(); return callMain(argv[0..argc], envp); @@ -735,8 +678,8 @@ inline fn callMain(args: std.process.Args.Vector, environ: std.process.Environ.B defer arena_allocator.deinit(); var threaded: std.Io.Threaded = .init(gpa, .{ - .argv0 = if (@sizeOf(std.Io.Threaded.Argv0) != 0) .{ .value = args[0] } else .{}, - .environ = .{ .block = environ }, + .argv0 = .init(.{ .value = args }), + .environ = .{ .process_environ = .{ .block = environ } }, }); defer threaded.deinit();