authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-06 20:43:57-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-23 22:15:08-08:00
log877032ec6a0007316f42658d12042f1473de4856
treed6541c2050b24f97ef80895061e881bc31fc5449
parent8328de24f13e21e325207b19288a143854df50df

std: reorganize realpath and process executable APIs


12 files changed, 333 insertions(+), 315 deletions(-)

lib/std/Io.zig+3-1
......@@ -697,7 +697,6 @@ pub const VTable = struct {
697697 fileReadPositional: *const fn (?*anyopaque, File, data: [][]u8, offset: u64) File.ReadPositionalError!usize,
698698 fileSeekBy: *const fn (?*anyopaque, File, relative_offset: i64) File.SeekError!void,
699699 fileSeekTo: *const fn (?*anyopaque, File, absolute_offset: u64) File.SeekError!void,
700 openSelfExe: *const fn (?*anyopaque, File.OpenFlags) File.OpenSelfExeError!File,
701700 fileSync: *const fn (?*anyopaque, File) File.SyncError!void,
702701 fileIsTty: *const fn (?*anyopaque, File) Cancelable!bool,
703702 fileEnableAnsiEscapeCodes: *const fn (?*anyopaque, File) File.EnableAnsiEscapeCodesError!void,
......@@ -712,6 +711,9 @@ pub const VTable = struct {
712711 fileUnlock: *const fn (?*anyopaque, File) void,
713712 fileDowngradeLock: *const fn (?*anyopaque, File) File.DowngradeLockError!void,
714713
714 processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,
715 processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,
716
715717 now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp,
716718 sleep: *const fn (?*anyopaque, Timeout) SleepError!void,
717719
lib/std/Io/Dir.zig+27-12
......@@ -694,24 +694,29 @@ pub const RealPathError = error{
694694/// supported hosts are: Linux, macOS, and Windows.
695695///
696696/// See also:
697/// * `realpathAlloc`.
697/// * `realPathAlloc`.
698698pub fn realPath(dir: Dir, io: Io, sub_path: []const u8, out_buffer: []u8) RealPathError!usize {
699699 return io.vtable.dirRealPath(io.userdata, dir, sub_path, out_buffer);
700700}
701701
702702pub const RealPathAllocError = RealPathError || Allocator.Error;
703703
704/// Same as `Dir.realpath` except caller must free the returned memory.
705/// See also `Dir.realpath`.
706pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) RealPathAllocError![]u8 {
707 // Use of max_path_bytes here is valid as the realpath function does not
708 // have a variant that takes an arbitrary-size buffer.
709 // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
710 // NULL out parameter (GNU's canonicalize_file_name) to handle overelong
711 // paths. musl supports passing NULL but restricts the output to PATH_MAX
712 // anyway.
713 var buf: [std.fs.max_path_bytes]u8 = undefined;
714 return allocator.dupe(u8, try self.realpath(pathname, &buf));
704/// Same as `realPath` except allocates result.
705pub fn realPathAlloc(dir: Dir, io: Io, sub_path: []const u8, allocator: Allocator) RealPathAllocError![:0]u8 {
706 var buffer: [std.fs.max_path_bytes]u8 = undefined;
707 const n = try realPath(dir, io, sub_path, &buffer);
708 return allocator.dupeZ(u8, buffer[0..n]);
709}
710
711pub fn realPathAbsolute(io: Io, path: []const u8, out_buffer: []u8) RealPathError!usize {
712 return io.vtable.dirRealPath(io.userdata, .cwd(), path, out_buffer);
713}
714
715/// Same as `realPathAbsolute` except allocates result.
716pub fn realPathAbsoluteAlloc(io: Io, path: []const u8, allocator: Allocator) RealPathAllocError![:0]u8 {
717 var buffer: [std.fs.max_path_bytes]u8 = undefined;
718 const n = try realPathAbsolute(io, path, &buffer);
719 return allocator.dupeZ(u8, buffer[0..n]);
715720}
716721
717722pub const DeleteFileError = error{
......@@ -975,6 +980,16 @@ pub fn readLink(dir: Dir, io: Io, sub_path: []const u8, buffer: []u8) ReadLinkEr
975980 return io.vtable.dirReadLink(io.userdata, dir, sub_path, buffer);
976981}
977982
983/// Same as `readLink`, except it asserts the path is absolute.
984///
985/// On Windows, `path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
986/// On WASI, `path` should be encoded as valid UTF-8.
987/// On other platforms, `path` is an opaque sequence of bytes with no particular encoding.
988pub fn readLinkAbsolute(io: Io, path: []const u8, buffer: []u8) ReadLinkError!usize {
989 assert(std.fs.path.isAbsolute(path));
990 return io.vtable.dirReadLink(io.userdata, .cwd(), path, buffer);
991}
992
978993pub const ReadFileAllocError = File.OpenError || File.ReadError || Allocator.Error || error{
979994 /// File size reached or exceeded the provided limit.
980995 StreamTooLong,
lib/std/Io/File.zig-6
......@@ -464,12 +464,6 @@ pub fn setTimestampsNow(file: File, io: Io) SetTimestampsError!void {
464464 return io.vtable.fileSetTimestampsNow(io.userdata, file);
465465}
466466
467pub const OpenSelfExeError = OpenError || std.fs.SelfExePathError || LockError;
468
469pub fn openSelfExe(io: Io, flags: OpenFlags) OpenSelfExeError!File {
470 return io.vtable.openSelfExe(io.userdata, flags);
471}
472
473467pub const ReadPositionalError = Reader.Error || error{Unseekable};
474468
475469pub fn readPositional(file: File, io: Io, buffer: [][]u8, offset: u64) ReadPositionalError!usize {
lib/std/Io/Kqueue.zig+2-2
......@@ -888,7 +888,7 @@ pub fn io(k: *Kqueue) Io {
888888 .fileReadPositional = fileReadPositional,
889889 .fileSeekBy = fileSeekBy,
890890 .fileSeekTo = fileSeekTo,
891 .openSelfExe = openSelfExe,
891 .openExecutable = openExecutable,
892892
893893 .now = now,
894894 .sleep = sleep,
......@@ -1246,7 +1246,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: File, absolute_offset: u64) File.Seek
12461246 _ = absolute_offset;
12471247 @panic("TODO");
12481248}
1249fn openSelfExe(userdata: ?*anyopaque, file: File.OpenFlags) File.OpenSelfExeError!File {
1249fn openExecutable(userdata: ?*anyopaque, file: File.OpenFlags) File.OpenExecutableError!File {
12501250 const k: *Kqueue = @ptrCast(@alignCast(userdata));
12511251 _ = k;
12521252 _ = file;
lib/std/Io/Threaded.zig+148-4
......@@ -717,7 +717,6 @@ pub fn io(t: *Threaded) Io {
717717 .fileReadPositional = fileReadPositional,
718718 .fileSeekBy = fileSeekBy,
719719 .fileSeekTo = fileSeekTo,
720 .openSelfExe = openSelfExe,
721720 .fileSync = fileSync,
722721 .fileIsTty = fileIsTty,
723722 .fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes,
......@@ -732,6 +731,9 @@ pub fn io(t: *Threaded) Io {
732731 .fileUnlock = fileUnlock,
733732 .fileDowngradeLock = fileDowngradeLock,
734733
734 .processExecutableOpen = processExecutableOpen,
735 .processExecutablePath = processExecutablePath,
736
735737 .now = now,
736738 .sleep = sleep,
737739
......@@ -839,7 +841,6 @@ pub fn ioBasic(t: *Threaded) Io {
839841 .fileReadPositional = fileReadPositional,
840842 .fileSeekBy = fileSeekBy,
841843 .fileSeekTo = fileSeekTo,
842 .openSelfExe = openSelfExe,
843844 .fileSync = fileSync,
844845 .fileIsTty = fileIsTty,
845846 .fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes,
......@@ -854,6 +855,9 @@ pub fn ioBasic(t: *Threaded) Io {
854855 .fileUnlock = fileUnlock,
855856 .fileDowngradeLock = fileDowngradeLock,
856857
858 .processExecutableOpen = processExecutableOpen,
859 .processExecutablePath = processExecutablePath,
860
857861 .now = now,
858862 .sleep = sleep,
859863
......@@ -5932,7 +5936,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: File, offset: u64) File.SeekError!voi
59325936 }
59335937}
59345938
5935fn openSelfExe(userdata: ?*anyopaque, flags: File.OpenFlags) File.OpenSelfExeError!File {
5939fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.process.OpenExecutableError!File {
59365940 const t: *Threaded = @ptrCast(@alignCast(userdata));
59375941 switch (native_os) {
59385942 .linux, .serenity => return dirOpenFilePosix(t, .{ .handle = posix.AT.FDCWD }, "/proc/self/exe", flags),
......@@ -5945,7 +5949,147 @@ fn openSelfExe(userdata: ?*anyopaque, flags: File.OpenFlags) File.OpenSelfExeErr
59455949 const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name);
59465950 return dirOpenFileWtf16(t, null, prefixed_path_w.span(), flags);
59475951 },
5948 else => @panic("TODO implement openSelfExe"),
5952 else => @panic("TODO implement processExecutableOpen"),
5953 }
5954}
5955
5956fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.ExecutablePathError!usize {
5957 const t: *Threaded = @ptrCast(@alignCast(userdata));
5958 const max_path_bytes = std.fs.max_path_bytes;
5959
5960 switch (native_os) {
5961 .driverkit,
5962 .ios,
5963 .maccatalyst,
5964 .macos,
5965 .tvos,
5966 .visionos,
5967 .watchos,
5968 => {
5969 // Note that _NSGetExecutablePath() will return "a path" to
5970 // the executable not a "real path" to the executable.
5971 var symlink_path_buf: [max_path_bytes:0]u8 = undefined;
5972 var u32_len: u32 = max_path_bytes + 1; // include the sentinel
5973 const rc = std.c._NSGetExecutablePath(&symlink_path_buf, &u32_len);
5974 if (rc != 0) return error.NameTooLong;
5975
5976 var real_path_buf: [max_path_bytes]u8 = undefined;
5977 const real_path = Io.Dir.realPathAbsolute(ioBasic(t), &symlink_path_buf, &real_path_buf) catch |err| switch (err) {
5978 error.NetworkNotFound => unreachable, // Windows-only
5979 else => |e| return e,
5980 };
5981 if (real_path.len > out_buffer.len) return error.NameTooLong;
5982 const result = out_buffer[0..real_path.len];
5983 @memcpy(result, real_path);
5984 return result.len;
5985 },
5986 .linux, .serenity => return Io.Dir.readLinkAbsolute(ioBasic(t), "/proc/self/exe", out_buffer) catch |err| switch (err) {
5987 error.UnsupportedReparsePointType => unreachable, // Windows-only
5988 error.NetworkNotFound => unreachable, // Windows-only
5989 else => |e| return e,
5990 },
5991 .illumos => return Io.Dir.readLinkAbsolute(ioBasic(t), "/proc/self/path/a.out", out_buffer) catch |err| switch (err) {
5992 error.UnsupportedReparsePointType => unreachable, // Windows-only
5993 error.NetworkNotFound => unreachable, // Windows-only
5994 else => |e| return e,
5995 },
5996 .freebsd, .dragonfly => {
5997 const current_thread = Thread.getCurrent(t);
5998 try current_thread.checkCancel();
5999 var mib: [4]c_int = .{ posix.CTL.KERN, posix.KERN.PROC, posix.KERN.PROC_PATHNAME, -1 };
6000 var out_len: usize = out_buffer.len;
6001 try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0);
6002 return out_len;
6003 },
6004 .netbsd => {
6005 const current_thread = Thread.getCurrent(t);
6006 try current_thread.checkCancel();
6007 var mib = [4]c_int{ posix.CTL.KERN, posix.KERN.PROC_ARGS, -1, posix.KERN.PROC_PATHNAME };
6008 var out_len: usize = out_buffer.len;
6009 try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0);
6010 return out_len;
6011 },
6012 .openbsd, .haiku => {
6013 // OpenBSD doesn't support getting the path of a running process, so try to guess it
6014 if (std.os.argv.len == 0)
6015 return error.FileNotFound;
6016
6017 const argv0 = std.mem.span(std.os.argv[0]);
6018 if (std.mem.indexOf(u8, argv0, "/") != null) {
6019 // argv[0] is a path (relative or absolute): use realpath(3) directly
6020 var real_path_buf: [max_path_bytes]u8 = undefined;
6021 const real_path = Io.Dir.realPathAbsolute(ioBasic(t), std.os.argv[0], &real_path_buf) catch |err| switch (err) {
6022 error.NetworkNotFound => unreachable, // Windows-only
6023 else => |e| return e,
6024 };
6025 if (real_path.len > out_buffer.len)
6026 return error.NameTooLong;
6027 const result = out_buffer[0..real_path.len];
6028 @memcpy(result, real_path);
6029 return result.len;
6030 } else if (argv0.len != 0) {
6031 // argv[0] is not empty (and not a path): search it inside PATH
6032 const PATH = posix.getenvZ("PATH") orelse return error.FileNotFound;
6033 var path_it = std.mem.tokenizeScalar(u8, PATH, std.fs.path.delimiter);
6034 while (path_it.next()) |a_path| {
6035 var resolved_path_buf: [max_path_bytes - 1:0]u8 = undefined;
6036 const resolved_path = std.fmt.bufPrintSentinel(&resolved_path_buf, "{s}/{s}", .{
6037 a_path, std.os.argv[0],
6038 }, 0) catch continue;
6039
6040 var real_path_buf: [max_path_bytes]u8 = undefined;
6041 if (Io.Dir.realPathAbsolute(ioBasic(t), resolved_path, &real_path_buf)) |real_path| {
6042 // found a file, and hope it is the right file
6043 if (real_path.len > out_buffer.len)
6044 return error.NameTooLong;
6045 const result = out_buffer[0..real_path.len];
6046 @memcpy(result, real_path);
6047 return result.len;
6048 } else |_| continue;
6049 }
6050 }
6051 return error.FileNotFound;
6052 },
6053 .windows => {
6054 const current_thread = Thread.getCurrent(t);
6055 try current_thread.checkCancel();
6056 const w = windows;
6057 const image_path_unicode_string = &w.peb().ProcessParameters.ImagePathName;
6058 const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0];
6059
6060 // If ImagePathName is a symlink, then it will contain the path of the
6061 // symlink, not the path that the symlink points to. We want the path
6062 // that the symlink points to, though, so we need to get the realpath.
6063 var path_name_w = try w.wToPrefixedFileW(null, image_path_name);
6064
6065 const access_mask = w.GENERIC_READ | w.SYNCHRONIZE;
6066 const share_access = w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE;
6067 const creation = w.FILE_OPEN;
6068 const h_file = blk: {
6069 const res = w.OpenFile(path_name_w.span(), .{
6070 .dir = null,
6071 .access_mask = access_mask,
6072 .share_access = share_access,
6073 .creation = creation,
6074 .filter = .any,
6075 }) catch |err| switch (err) {
6076 error.WouldBlock => unreachable,
6077 else => |e| return e,
6078 };
6079 break :blk res;
6080 };
6081 defer w.CloseHandle(h_file);
6082
6083 const wide_slice = w.GetFinalPathNameByHandle(h_file, .{}, out_buffer);
6084
6085 const len = std.unicode.calcWtf8Len(wide_slice);
6086 if (len > out_buffer.len)
6087 return error.NameTooLong;
6088
6089 const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
6090 return end_index;
6091 },
6092 else => @compileError("unsupported OS"),
59496093 }
59506094}
59516095
lib/std/debug/SelfInfo/Elf.zig+1-1
......@@ -329,7 +329,7 @@ const Module = struct {
329329 defer file.close(io);
330330 break :res std.debug.ElfFile.load(gpa, file, mod.build_id, &.native(mod.name));
331331 } else res: {
332 const path = std.fs.selfExePathAlloc(gpa) catch |err| switch (err) {
332 const path = std.process.executablePathAlloc(io, gpa) catch |err| switch (err) {
333333 error.OutOfMemory => |e| return e,
334334 else => return error.ReadFailed,
335335 };
lib/std/debug/SelfInfo/Windows.zig+1-1
......@@ -434,7 +434,7 @@ const Module = struct {
434434 const pdb_file_open_result = if (fs.path.isAbsolute(path)) res: {
435435 break :res std.fs.cwd().openFile(io, path, .{});
436436 } else res: {
437 const self_dir = fs.selfExeDirPathAlloc(gpa) catch |err| switch (err) {
437 const self_dir = std.process.executableDirPathAlloc(io, gpa) catch |err| switch (err) {
438438 error.OutOfMemory, error.Unexpected => |e| return e,
439439 else => return error.ReadFailed,
440440 };
lib/std/fs.zig-232
......@@ -25,13 +25,6 @@ pub const File = std.Io.File;
2525pub const path = @import("fs/path.zig");
2626pub const wasi = @import("fs/wasi.zig");
2727
28// TODO audit these APIs with respect to Dir and absolute paths
29
30pub const realpath = posix.realpath;
31pub const realpathZ = posix.realpathZ;
32pub const realpathW = posix.realpathW;
33pub const realpathW2 = posix.realpathW2;
34
3528pub const getAppDataDir = @import("fs/get_app_data_dir.zig").getAppDataDir;
3629pub const GetAppDataDirError = @import("fs/get_app_data_dir.zig").GetAppDataDirError;
3730
......@@ -241,15 +234,6 @@ pub fn deleteTreeAbsolute(io: Io, absolute_path: []const u8) !void {
241234 return dir.deleteTree(path.basename(absolute_path));
242235}
243236
244/// Same as `Dir.readLink`, except it asserts the path is absolute.
245/// On Windows, `pathname` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
246/// On WASI, `pathname` should be encoded as valid UTF-8.
247/// On other platforms, `pathname` is an opaque sequence of bytes with no particular encoding.
248pub fn readLinkAbsolute(pathname: []const u8, buffer: *[max_path_bytes]u8) ![]u8 {
249 assert(path.isAbsolute(pathname));
250 return posix.readlink(pathname, buffer);
251}
252
253237/// Creates a symbolic link named `sym_link_path` which contains the string `target_path`.
254238/// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent
255239/// one; the latter case is known as a dangling link.
......@@ -287,222 +271,6 @@ pub fn symLinkAbsoluteW(
287271 return windows.CreateSymbolicLink(null, mem.span(sym_link_path_w), mem.span(target_path_w), flags.is_directory);
288272}
289273
290// This is `posix.ReadLinkError || posix.RealPathError` with impossible errors excluded
291pub const SelfExePathError = error{
292 FileNotFound,
293 AccessDenied,
294 NameTooLong,
295 NotSupported,
296 NotDir,
297 SymLinkLoop,
298 InputOutput,
299 FileTooBig,
300 IsDir,
301 ProcessFdQuotaExceeded,
302 SystemFdQuotaExceeded,
303 NoDevice,
304 SystemResources,
305 NoSpaceLeft,
306 FileSystem,
307 BadPathName,
308 DeviceBusy,
309 SharingViolation,
310 PipeBusy,
311 NotLink,
312 PathAlreadyExists,
313
314 /// On Windows, `\\server` or `\\server\share` was not found.
315 NetworkNotFound,
316 ProcessNotFound,
317
318 /// On Windows, antivirus software is enabled by default. It can be
319 /// disabled, but Windows Update sometimes ignores the user's preference
320 /// and re-enables it. When enabled, antivirus software on Windows
321 /// intercepts file system operations and makes them significantly slower
322 /// in addition to possibly failing with this error code.
323 AntivirusInterference,
324
325 /// On Windows, the volume does not contain a recognized file system. File
326 /// system drivers might not be loaded, or the volume may be corrupt.
327 UnrecognizedVolume,
328
329 Canceled,
330} || posix.SysCtlError;
331
332/// `selfExePath` except allocates the result on the heap.
333/// Caller owns returned memory.
334pub fn selfExePathAlloc(allocator: Allocator) ![]u8 {
335 // Use of max_path_bytes here is justified as, at least on one tested Linux
336 // system, readlink will completely fail to return a result larger than
337 // PATH_MAX even if given a sufficiently large buffer. This makes it
338 // fundamentally impossible to get the selfExePath of a program running in
339 // a very deeply nested directory chain in this way.
340 // TODO(#4812): Investigate other systems and whether it is possible to get
341 // this path by trying larger and larger buffers until one succeeds.
342 var buf: [max_path_bytes]u8 = undefined;
343 return allocator.dupe(u8, try selfExePath(&buf));
344}
345
346/// Get the path to the current executable. Follows symlinks.
347/// If you only need the directory, use selfExeDirPath.
348/// If you only want an open file handle, use openSelfExe.
349/// This function may return an error if the current executable
350/// was deleted after spawning.
351/// Returned value is a slice of out_buffer.
352/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
353/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
354///
355/// On Linux, depends on procfs being mounted. If the currently executing binary has
356/// been deleted, the file path looks something like `/a/b/c/exe (deleted)`.
357/// TODO make the return type of this a null terminated pointer
358pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 {
359 if (is_darwin) {
360 // Note that _NSGetExecutablePath() will return "a path" to
361 // the executable not a "real path" to the executable.
362 var symlink_path_buf: [max_path_bytes:0]u8 = undefined;
363 var u32_len: u32 = max_path_bytes + 1; // include the sentinel
364 const rc = std.c._NSGetExecutablePath(&symlink_path_buf, &u32_len);
365 if (rc != 0) return error.NameTooLong;
366
367 var real_path_buf: [max_path_bytes]u8 = undefined;
368 const real_path = std.posix.realpathZ(&symlink_path_buf, &real_path_buf) catch |err| switch (err) {
369 error.NetworkNotFound => unreachable, // Windows-only
370 else => |e| return e,
371 };
372 if (real_path.len > out_buffer.len) return error.NameTooLong;
373 const result = out_buffer[0..real_path.len];
374 @memcpy(result, real_path);
375 return result;
376 }
377 switch (native_os) {
378 .linux, .serenity => return posix.readlinkZ("/proc/self/exe", out_buffer) catch |err| switch (err) {
379 error.UnsupportedReparsePointType => unreachable, // Windows-only
380 error.NetworkNotFound => unreachable, // Windows-only
381 else => |e| return e,
382 },
383 .illumos => return posix.readlinkZ("/proc/self/path/a.out", out_buffer) catch |err| switch (err) {
384 error.UnsupportedReparsePointType => unreachable, // Windows-only
385 error.NetworkNotFound => unreachable, // Windows-only
386 else => |e| return e,
387 },
388 .freebsd, .dragonfly => {
389 var mib = [4]c_int{ posix.CTL.KERN, posix.KERN.PROC, posix.KERN.PROC_PATHNAME, -1 };
390 var out_len: usize = out_buffer.len;
391 try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0);
392 // TODO could this slice from 0 to out_len instead?
393 return mem.sliceTo(out_buffer, 0);
394 },
395 .netbsd => {
396 var mib = [4]c_int{ posix.CTL.KERN, posix.KERN.PROC_ARGS, -1, posix.KERN.PROC_PATHNAME };
397 var out_len: usize = out_buffer.len;
398 try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0);
399 // TODO could this slice from 0 to out_len instead?
400 return mem.sliceTo(out_buffer, 0);
401 },
402 .openbsd, .haiku => {
403 // OpenBSD doesn't support getting the path of a running process, so try to guess it
404 if (std.os.argv.len == 0)
405 return error.FileNotFound;
406
407 const argv0 = mem.span(std.os.argv[0]);
408 if (mem.find(u8, argv0, "/") != null) {
409 // argv[0] is a path (relative or absolute): use realpath(3) directly
410 var real_path_buf: [max_path_bytes]u8 = undefined;
411 const real_path = posix.realpathZ(std.os.argv[0], &real_path_buf) catch |err| switch (err) {
412 error.NetworkNotFound => unreachable, // Windows-only
413 else => |e| return e,
414 };
415 if (real_path.len > out_buffer.len)
416 return error.NameTooLong;
417 const result = out_buffer[0..real_path.len];
418 @memcpy(result, real_path);
419 return result;
420 } else if (argv0.len != 0) {
421 // argv[0] is not empty (and not a path): search it inside PATH
422 const PATH = posix.getenvZ("PATH") orelse return error.FileNotFound;
423 var path_it = mem.tokenizeScalar(u8, PATH, path.delimiter);
424 while (path_it.next()) |a_path| {
425 var resolved_path_buf: [max_path_bytes - 1:0]u8 = undefined;
426 const resolved_path = std.fmt.bufPrintSentinel(&resolved_path_buf, "{s}/{s}", .{
427 a_path,
428 std.os.argv[0],
429 }, 0) catch continue;
430
431 var real_path_buf: [max_path_bytes]u8 = undefined;
432 if (posix.realpathZ(resolved_path, &real_path_buf)) |real_path| {
433 // found a file, and hope it is the right file
434 if (real_path.len > out_buffer.len)
435 return error.NameTooLong;
436 const result = out_buffer[0..real_path.len];
437 @memcpy(result, real_path);
438 return result;
439 } else |_| continue;
440 }
441 }
442 return error.FileNotFound;
443 },
444 .windows => {
445 const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName;
446 const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0];
447
448 // If ImagePathName is a symlink, then it will contain the path of the
449 // symlink, not the path that the symlink points to. We want the path
450 // that the symlink points to, though, so we need to get the realpath.
451 var pathname_w = try windows.wToPrefixedFileW(null, image_path_name);
452
453 const wide_slice = try std.fs.cwd().realpathW2(pathname_w.span(), &pathname_w.data);
454
455 const len = std.unicode.calcWtf8Len(wide_slice);
456 if (len > out_buffer.len)
457 return error.NameTooLong;
458
459 const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
460 return out_buffer[0..end_index];
461 },
462 else => @compileError("std.fs.selfExePath not supported for this target"),
463 }
464}
465
466/// `selfExeDirPath` except allocates the result on the heap.
467/// Caller owns returned memory.
468pub fn selfExeDirPathAlloc(allocator: Allocator) ![]u8 {
469 // Use of max_path_bytes here is justified as, at least on one tested Linux
470 // system, readlink will completely fail to return a result larger than
471 // PATH_MAX even if given a sufficiently large buffer. This makes it
472 // fundamentally impossible to get the selfExeDirPath of a program running
473 // in a very deeply nested directory chain in this way.
474 // TODO(#4812): Investigate other systems and whether it is possible to get
475 // this path by trying larger and larger buffers until one succeeds.
476 var buf: [max_path_bytes]u8 = undefined;
477 return allocator.dupe(u8, try selfExeDirPath(&buf));
478}
479
480/// Get the directory path that contains the current executable.
481/// Returned value is a slice of out_buffer.
482/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
483/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
484pub fn selfExeDirPath(out_buffer: []u8) SelfExePathError![]const u8 {
485 const self_exe_path = try selfExePath(out_buffer);
486 // Assume that the OS APIs return absolute paths, and therefore dirname
487 // will not return null.
488 return path.dirname(self_exe_path).?;
489}
490
491/// `realpath`, except caller must free the returned memory.
492/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
493/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
494/// See also `Dir.realpath`.
495pub fn realpathAlloc(allocator: Allocator, pathname: []const u8) ![]u8 {
496 // Use of max_path_bytes here is valid as the realpath function does not
497 // have a variant that takes an arbitrary-size buffer.
498 // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
499 // NULL out parameter (GNU's canonicalize_file_name) to handle overelong
500 // paths. musl supports passing NULL but restricts the output to PATH_MAX
501 // anyway.
502 var buf: [max_path_bytes]u8 = undefined;
503 return allocator.dupe(u8, try posix.realpath(pathname, &buf));
504}
505
506274test {
507275 _ = AtomicFile;
508276 _ = Dir;
lib/std/fs/test.zig+50-5
......@@ -4,6 +4,7 @@ const native_os = builtin.os.tag;
44const std = @import("../std.zig");
55const Io = std.Io;
66const testing = std.testing;
7const expect = std.testing.expect;
78const fs = std.fs;
89const mem = std.mem;
910const wasi = std.os.wasi;
......@@ -1177,21 +1178,22 @@ test "renameAbsolute" {
11771178 dir.close(io);
11781179}
11791180
1180test "openSelfExe" {
1181test "openExecutable" {
11811182 if (native_os == .wasi) return error.SkipZigTest;
11821183
11831184 const io = testing.io;
11841185
1185 const self_exe_file = try std.fs.openSelfExe(.{});
1186 const self_exe_file = try std.fs.openExecutable(.{});
11861187 self_exe_file.close(io);
11871188}
11881189
1189test "selfExePath" {
1190test "executablePath" {
11901191 if (native_os == .wasi) return error.SkipZigTest;
11911192
1193 const io = testing.io;
11921194 var buf: [fs.max_path_bytes]u8 = undefined;
1193 const buf_self_exe_path = try std.fs.selfExePath(&buf);
1194 const alloc_self_exe_path = try std.fs.selfExePathAlloc(testing.allocator);
1195 const buf_self_exe_path = try std.process.executablePath(io, &buf);
1196 const alloc_self_exe_path = try std.process.executablePathAlloc(io, testing.allocator);
11951197 defer testing.allocator.free(alloc_self_exe_path);
11961198 try testing.expectEqualSlices(u8, buf_self_exe_path, alloc_self_exe_path);
11971199}
......@@ -2371,3 +2373,46 @@ test "File.Writer sendfile with buffered contents" {
23712373 try testing.expectEqualStrings("abcd", try check_r.interface.take(4));
23722374 try testing.expectError(error.EndOfStream, check_r.interface.takeByte());
23732375}
2376
2377test "readlink on Windows" {
2378 if (native_os != .windows) return error.SkipZigTest;
2379
2380 try testReadlink("C:\\ProgramData", "C:\\Users\\All Users");
2381 try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User");
2382 try testReadlink("C:\\Users", "C:\\Documents and Settings");
2383}
2384
2385fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void {
2386 var buffer: [fs.max_path_bytes]u8 = undefined;
2387 const given = try Dir.readLinkAbsolute(symlink_path, buffer[0..]);
2388 try expect(mem.eql(u8, target_path, given));
2389}
2390
2391test "readlinkat" {
2392 var tmp = tmpDir(.{});
2393 defer tmp.cleanup();
2394
2395 // create file
2396 try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" });
2397
2398 // create a symbolic link
2399 if (native_os == .windows) {
2400 std.os.windows.CreateSymbolicLink(
2401 tmp.dir.fd,
2402 &[_]u16{ 'l', 'i', 'n', 'k' },
2403 &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
2404 false,
2405 ) catch |err| switch (err) {
2406 // Symlink requires admin privileges on windows, so this test can legitimately fail.
2407 error.AccessDenied => return error.SkipZigTest,
2408 else => return err,
2409 };
2410 } else {
2411 try posix.symlinkat("file.txt", tmp.dir.fd, "link");
2412 }
2413
2414 // read the link
2415 var buffer: [fs.max_path_bytes]u8 = undefined;
2416 const read_link = try tmp.dir.readLink("link", &buffer);
2417 try expect(mem.eql(u8, "file.txt", read_link));
2418}
lib/std/posix/test.zig-43
......@@ -111,20 +111,6 @@ test "open smoke test" {
111111 }
112112}
113113
114test "readlink on Windows" {
115 if (native_os != .windows) return error.SkipZigTest;
116
117 try testReadlink("C:\\ProgramData", "C:\\Users\\All Users");
118 try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User");
119 try testReadlink("C:\\Users", "C:\\Documents and Settings");
120}
121
122fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void {
123 var buffer: [fs.max_path_bytes]u8 = undefined;
124 const given = try posix.readlink(symlink_path, buffer[0..]);
125 try expect(mem.eql(u8, target_path, given));
126}
127
128114fn getLinkInfo(fd: posix.fd_t) !struct { posix.ino_t, posix.nlink_t } {
129115 if (native_os == .linux) {
130116 const stx = try linux.wrapped.statx(
......@@ -216,35 +202,6 @@ test "fstatat" {
216202 // try expectEqual(stat.blocks, statat.blocks);
217203}
218204
219test "readlinkat" {
220 var tmp = tmpDir(.{});
221 defer tmp.cleanup();
222
223 // create file
224 try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" });
225
226 // create a symbolic link
227 if (native_os == .windows) {
228 std.os.windows.CreateSymbolicLink(
229 tmp.dir.fd,
230 &[_]u16{ 'l', 'i', 'n', 'k' },
231 &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
232 false,
233 ) catch |err| switch (err) {
234 // Symlink requires admin privileges on windows, so this test can legitimately fail.
235 error.AccessDenied => return error.SkipZigTest,
236 else => return err,
237 };
238 } else {
239 try posix.symlinkat("file.txt", tmp.dir.fd, "link");
240 }
241
242 // read the link
243 var buffer: [fs.max_path_bytes]u8 = undefined;
244 const read_link = try posix.readlinkat(tmp.dir.fd, "link", buffer[0..]);
245 try expect(mem.eql(u8, "file.txt", read_link));
246}
247
248205test "getrandom" {
249206 var buf_a: [50]u8 = undefined;
250207 var buf_b: [50]u8 = undefined;
lib/std/process.zig+97-1
......@@ -3,6 +3,7 @@ const native_os = builtin.os.tag;
33
44const std = @import("std.zig");
55const Io = std.Io;
6const File = std.Io.File;
67const fs = std.fs;
78const mem = std.mem;
89const math = std.math;
......@@ -12,6 +13,7 @@ const testing = std.testing;
1213const posix = std.posix;
1314const windows = std.os.windows;
1415const unicode = std.unicode;
16const max_path_bytes = std.fs.max_path_bytes;
1517
1618pub const Child = @import("process/Child.zig");
1719pub const abort = posix.abort;
......@@ -37,7 +39,7 @@ pub const GetCwdAllocError = Allocator.Error || error{CurrentWorkingDirectoryUnl
3739pub fn getCwdAlloc(allocator: Allocator) GetCwdAllocError![]u8 {
3840 // The use of max_path_bytes here is just a heuristic: most paths will fit
3941 // in stack_buf, avoiding an extra allocation in the common case.
40 var stack_buf: [fs.max_path_bytes]u8 = undefined;
42 var stack_buf: [max_path_bytes]u8 = undefined;
4143 var heap_buf: ?[]u8 = null;
4244 defer if (heap_buf) |buf| allocator.free(buf);
4345
......@@ -2112,3 +2114,97 @@ pub fn fatal(comptime format: []const u8, format_arguments: anytype) noreturn {
21122114 std.log.err(format, format_arguments);
21132115 exit(1);
21142116}
2117
2118pub const ExecutablePathBaseError = error{
2119 FileNotFound,
2120 AccessDenied,
2121 NotSupported,
2122 NotDir,
2123 SymLinkLoop,
2124 InputOutput,
2125 FileTooBig,
2126 IsDir,
2127 ProcessFdQuotaExceeded,
2128 SystemFdQuotaExceeded,
2129 NoDevice,
2130 SystemResources,
2131 NoSpaceLeft,
2132 FileSystem,
2133 BadPathName,
2134 DeviceBusy,
2135 SharingViolation,
2136 PipeBusy,
2137 NotLink,
2138 PathAlreadyExists,
2139 /// On Windows, `\\server` or `\\server\share` was not found.
2140 NetworkNotFound,
2141 ProcessNotFound,
2142 /// On Windows, antivirus software is enabled by default. It can be
2143 /// disabled, but Windows Update sometimes ignores the user's preference
2144 /// and re-enables it. When enabled, antivirus software on Windows
2145 /// intercepts file system operations and makes them significantly slower
2146 /// in addition to possibly failing with this error code.
2147 AntivirusInterference,
2148 /// On Windows, the volume does not contain a recognized file system. File
2149 /// system drivers might not be loaded, or the volume may be corrupt.
2150 UnrecognizedVolume,
2151 PermissionDenied,
2152} || Io.Cancelable || Io.UnexpectedError;
2153
2154pub const ExecutablePathAllocError = ExecutablePathBaseError || Allocator.Error;
2155
2156pub fn executablePathAlloc(io: Io, allocator: Allocator) ExecutablePathAllocError![:0]u8 {
2157 var buffer: [max_path_bytes]u8 = undefined;
2158 const n = executablePath(io, &buffer) catch |err| switch (err) {
2159 error.NameTooLong => unreachable,
2160 else => |e| return e,
2161 };
2162 return allocator.dupeZ(u8, buffer[0..n]);
2163}
2164
2165pub const ExecutablePathError = ExecutablePathBaseError || error{NameTooLong};
2166
2167/// Get the path to the current executable, following symlinks.
2168///
2169/// This function may return an error if the current executable
2170/// was deleted after spawning.
2171///
2172/// Returned value is a slice of out_buffer.
2173///
2174/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
2175/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
2176///
2177/// On Linux, depends on procfs being mounted. If the currently executing binary has
2178/// been deleted, the file path looks something like "/a/b/c/exe (deleted)".
2179///
2180/// See also:
2181/// * `executableDirPath` - to obtain only the directory
2182/// * `openExecutable` - to obtain only an open file handle
2183pub fn executablePath(io: Io, out_buffer: []u8) ExecutablePathError!usize {
2184 return io.vtable.processExecutablePath(io.userdata, out_buffer);
2185}
2186
2187/// Get the directory path that contains the current executable.
2188///
2189/// Returns index into `out_buffer`.
2190///
2191/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
2192/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
2193pub fn executableDirPath(out_buffer: []u8) ExecutablePathError!usize {
2194 const n = try executablePath(out_buffer);
2195 // Assert that the OS APIs return absolute paths, and therefore dirname
2196 // will not return null.
2197 return std.fs.path.dirname(out_buffer[0..n]).?;
2198}
2199
2200/// Same as `executableDirPath` except allocates the result.
2201pub fn executableDirPathAlloc(allocator: Allocator) ![]u8 {
2202 var buffer: [max_path_bytes]u8 = undefined;
2203 return allocator.dupe(u8, try executableDirPath(&buffer));
2204}
2205
2206pub const OpenExecutableError = File.OpenError || ExecutablePathError || File.LockError;
2207
2208pub fn openExecutable(io: Io, flags: File.OpenFlags) OpenExecutableError!File {
2209 return io.vtable.processExecutableOpen(io.userdata, flags);
2210}
lib/std/zig/system.zig+4-7
......@@ -209,7 +209,6 @@ pub const DetectError = error{
209209 DeviceBusy,
210210 OSVersionDetectionFail,
211211 Unexpected,
212 ProcessNotFound,
213212} || Io.Cancelable;
214213
215214/// Given a `Target.Query`, which specifies in detail which parts of the
......@@ -422,7 +421,6 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target {
422421 error.SocketUnconnected => return error.Unexpected,
423422
424423 error.AccessDenied,
425 error.ProcessNotFound,
426424 error.SymLinkLoop,
427425 error.ProcessFdQuotaExceeded,
428426 error.SystemFdQuotaExceeded,
......@@ -553,7 +551,6 @@ pub const AbiAndDynamicLinkerFromFileError = error{
553551 SystemResources,
554552 ProcessFdQuotaExceeded,
555553 SystemFdQuotaExceeded,
556 ProcessNotFound,
557554 IsDir,
558555 WouldBlock,
559556 InputOutput,
......@@ -693,8 +690,10 @@ fn abiAndDynamicLinkerFromFile(
693690
694691 // So far, no luck. Next we try to see if the information is
695692 // present in the symlink data for the dynamic linker path.
696 var link_buf: [posix.PATH_MAX]u8 = undefined;
697 const link_name = posix.readlink(dl_path, &link_buf) catch |err| switch (err) {
693 var link_buffer: [posix.PATH_MAX]u8 = undefined;
694 const link_name = if (Io.Dir.readLinkAbsolute(io, dl_path, &link_buffer)) |n|
695 link_buffer[0..n]
696 else |err| switch (err) {
698697 error.NameTooLong => unreachable,
699698 error.BadPathName => unreachable, // Windows only
700699 error.UnsupportedReparsePointType => unreachable, // Windows only
......@@ -839,7 +838,6 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion {
839838 error.NotDir => return error.GLibCNotFound,
840839 error.IsDir => return error.GLibCNotFound,
841840
842 error.ProcessNotFound => |e| return e,
843841 error.ProcessFdQuotaExceeded => |e| return e,
844842 error.SystemFdQuotaExceeded => |e| return e,
845843 error.SystemResources => |e| return e,
......@@ -1103,7 +1101,6 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ
11031101 error.SymLinkLoop,
11041102 error.ProcessFdQuotaExceeded,
11051103 error.SystemFdQuotaExceeded,
1106 error.ProcessNotFound,
11071104 error.Canceled,
11081105 => |e| return e,
11091106