diff --git a/lib/std/Io.zig b/lib/std/Io.zig index f783718cef16597326ecb0d912fb527ed9187013..17fb75fe5412a9b233711e15e1c8e24401c65d90 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -697,7 +697,6 @@ pub const VTable = struct { fileReadPositional: *const fn (?*anyopaque, File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, fileSeekBy: *const fn (?*anyopaque, File, relative_offset: i64) File.SeekError!void, fileSeekTo: *const fn (?*anyopaque, File, absolute_offset: u64) File.SeekError!void, - openSelfExe: *const fn (?*anyopaque, File.OpenFlags) File.OpenSelfExeError!File, fileSync: *const fn (?*anyopaque, File) File.SyncError!void, fileIsTty: *const fn (?*anyopaque, File) Cancelable!bool, fileEnableAnsiEscapeCodes: *const fn (?*anyopaque, File) File.EnableAnsiEscapeCodesError!void, @@ -712,6 +711,9 @@ pub const VTable = struct { fileUnlock: *const fn (?*anyopaque, File) void, fileDowngradeLock: *const fn (?*anyopaque, File) File.DowngradeLockError!void, + processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, + processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, + now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 9ae636d4a614e6427363ca16e9f375df85250d28..71ea3b465a23d504b11a0b9af2abb865e23b0272 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -694,24 +694,29 @@ pub const RealPathError = error{ /// supported hosts are: Linux, macOS, and Windows. /// /// See also: -/// * `realpathAlloc`. +/// * `realPathAlloc`. pub fn realPath(dir: Dir, io: Io, sub_path: []const u8, out_buffer: []u8) RealPathError!usize { return io.vtable.dirRealPath(io.userdata, dir, sub_path, out_buffer); } pub const RealPathAllocError = RealPathError || Allocator.Error; -/// Same as `Dir.realpath` except caller must free the returned memory. -/// See also `Dir.realpath`. -pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) RealPathAllocError![]u8 { - // Use of max_path_bytes here is valid as the realpath function does not - // have a variant that takes an arbitrary-size buffer. - // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 - // NULL out parameter (GNU's canonicalize_file_name) to handle overelong - // paths. musl supports passing NULL but restricts the output to PATH_MAX - // anyway. - var buf: [std.fs.max_path_bytes]u8 = undefined; - return allocator.dupe(u8, try self.realpath(pathname, &buf)); +/// Same as `realPath` except allocates result. +pub fn realPathAlloc(dir: Dir, io: Io, sub_path: []const u8, allocator: Allocator) RealPathAllocError![:0]u8 { + var buffer: [std.fs.max_path_bytes]u8 = undefined; + const n = try realPath(dir, io, sub_path, &buffer); + return allocator.dupeZ(u8, buffer[0..n]); +} + +pub fn realPathAbsolute(io: Io, path: []const u8, out_buffer: []u8) RealPathError!usize { + return io.vtable.dirRealPath(io.userdata, .cwd(), path, out_buffer); +} + +/// Same as `realPathAbsolute` except allocates result. +pub fn realPathAbsoluteAlloc(io: Io, path: []const u8, allocator: Allocator) RealPathAllocError![:0]u8 { + var buffer: [std.fs.max_path_bytes]u8 = undefined; + const n = try realPathAbsolute(io, path, &buffer); + return allocator.dupeZ(u8, buffer[0..n]); } pub const DeleteFileError = error{ @@ -975,6 +980,16 @@ pub fn readLink(dir: Dir, io: Io, sub_path: []const u8, buffer: []u8) ReadLinkEr return io.vtable.dirReadLink(io.userdata, dir, sub_path, buffer); } +/// Same as `readLink`, except it asserts the path is absolute. +/// +/// On Windows, `path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `path` should be encoded as valid UTF-8. +/// On other platforms, `path` is an opaque sequence of bytes with no particular encoding. +pub fn readLinkAbsolute(io: Io, path: []const u8, buffer: []u8) ReadLinkError!usize { + assert(std.fs.path.isAbsolute(path)); + return io.vtable.dirReadLink(io.userdata, .cwd(), path, buffer); +} + pub const ReadFileAllocError = File.OpenError || File.ReadError || Allocator.Error || error{ /// File size reached or exceeded the provided limit. StreamTooLong, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index a0349498c5f5027b47a1c364bcbc9546159b311f..b24c0b5100e896889104e2996551cbe774be273d 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -464,12 +464,6 @@ pub fn setTimestampsNow(file: File, io: Io) SetTimestampsError!void { return io.vtable.fileSetTimestampsNow(io.userdata, file); } -pub const OpenSelfExeError = OpenError || std.fs.SelfExePathError || LockError; - -pub fn openSelfExe(io: Io, flags: OpenFlags) OpenSelfExeError!File { - return io.vtable.openSelfExe(io.userdata, flags); -} - pub const ReadPositionalError = Reader.Error || error{Unseekable}; pub fn readPositional(file: File, io: Io, buffer: [][]u8, offset: u64) ReadPositionalError!usize { diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index 5b4f71da08cb7715177603696f8121c14adde000..e34c862ae99ec017aae3247e3d7037ed264c26fe 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -888,7 +888,7 @@ pub fn io(k: *Kqueue) Io { .fileReadPositional = fileReadPositional, .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, - .openSelfExe = openSelfExe, + .openExecutable = openExecutable, .now = now, .sleep = sleep, @@ -1246,7 +1246,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: File, absolute_offset: u64) File.Seek _ = absolute_offset; @panic("TODO"); } -fn openSelfExe(userdata: ?*anyopaque, file: File.OpenFlags) File.OpenSelfExeError!File { +fn openExecutable(userdata: ?*anyopaque, file: File.OpenFlags) File.OpenExecutableError!File { const k: *Kqueue = @ptrCast(@alignCast(userdata)); _ = k; _ = file; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 43424e348b0e4a7c3816307011d43193d77fc7f0..fb76002201428ff080c6e553d24a28e73d5a7bd3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -717,7 +717,6 @@ pub fn io(t: *Threaded) Io { .fileReadPositional = fileReadPositional, .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, - .openSelfExe = openSelfExe, .fileSync = fileSync, .fileIsTty = fileIsTty, .fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes, @@ -732,6 +731,9 @@ pub fn io(t: *Threaded) Io { .fileUnlock = fileUnlock, .fileDowngradeLock = fileDowngradeLock, + .processExecutableOpen = processExecutableOpen, + .processExecutablePath = processExecutablePath, + .now = now, .sleep = sleep, @@ -839,7 +841,6 @@ pub fn ioBasic(t: *Threaded) Io { .fileReadPositional = fileReadPositional, .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, - .openSelfExe = openSelfExe, .fileSync = fileSync, .fileIsTty = fileIsTty, .fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes, @@ -854,6 +855,9 @@ pub fn ioBasic(t: *Threaded) Io { .fileUnlock = fileUnlock, .fileDowngradeLock = fileDowngradeLock, + .processExecutableOpen = processExecutableOpen, + .processExecutablePath = processExecutablePath, + .now = now, .sleep = sleep, @@ -5932,7 +5936,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: File, offset: u64) File.SeekError!voi } } -fn openSelfExe(userdata: ?*anyopaque, flags: File.OpenFlags) File.OpenSelfExeError!File { +fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.process.OpenExecutableError!File { const t: *Threaded = @ptrCast(@alignCast(userdata)); switch (native_os) { .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 const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name); return dirOpenFileWtf16(t, null, prefixed_path_w.span(), flags); }, - else => @panic("TODO implement openSelfExe"), + else => @panic("TODO implement processExecutableOpen"), + } +} + +fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.ExecutablePathError!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const max_path_bytes = std.fs.max_path_bytes; + + switch (native_os) { + .driverkit, + .ios, + .maccatalyst, + .macos, + .tvos, + .visionos, + .watchos, + => { + // Note that _NSGetExecutablePath() will return "a path" to + // the executable not a "real path" to the executable. + var symlink_path_buf: [max_path_bytes:0]u8 = undefined; + var u32_len: u32 = max_path_bytes + 1; // include the sentinel + const rc = std.c._NSGetExecutablePath(&symlink_path_buf, &u32_len); + if (rc != 0) return error.NameTooLong; + + var real_path_buf: [max_path_bytes]u8 = undefined; + const real_path = Io.Dir.realPathAbsolute(ioBasic(t), &symlink_path_buf, &real_path_buf) catch |err| switch (err) { + error.NetworkNotFound => unreachable, // Windows-only + else => |e| return e, + }; + if (real_path.len > out_buffer.len) return error.NameTooLong; + const result = out_buffer[0..real_path.len]; + @memcpy(result, real_path); + return result.len; + }, + .linux, .serenity => return Io.Dir.readLinkAbsolute(ioBasic(t), "/proc/self/exe", out_buffer) catch |err| switch (err) { + error.UnsupportedReparsePointType => unreachable, // Windows-only + error.NetworkNotFound => unreachable, // Windows-only + else => |e| return e, + }, + .illumos => return Io.Dir.readLinkAbsolute(ioBasic(t), "/proc/self/path/a.out", out_buffer) catch |err| switch (err) { + error.UnsupportedReparsePointType => unreachable, // Windows-only + error.NetworkNotFound => unreachable, // Windows-only + else => |e| return e, + }, + .freebsd, .dragonfly => { + const current_thread = Thread.getCurrent(t); + try current_thread.checkCancel(); + var mib: [4]c_int = .{ posix.CTL.KERN, posix.KERN.PROC, posix.KERN.PROC_PATHNAME, -1 }; + var out_len: usize = out_buffer.len; + try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0); + return out_len; + }, + .netbsd => { + const current_thread = Thread.getCurrent(t); + try current_thread.checkCancel(); + var mib = [4]c_int{ posix.CTL.KERN, posix.KERN.PROC_ARGS, -1, posix.KERN.PROC_PATHNAME }; + var out_len: usize = out_buffer.len; + try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0); + return out_len; + }, + .openbsd, .haiku => { + // OpenBSD doesn't support getting the path of a running process, so try to guess it + if (std.os.argv.len == 0) + return error.FileNotFound; + + const argv0 = std.mem.span(std.os.argv[0]); + if (std.mem.indexOf(u8, argv0, "/") != null) { + // argv[0] is a path (relative or absolute): use realpath(3) directly + var real_path_buf: [max_path_bytes]u8 = undefined; + const real_path = Io.Dir.realPathAbsolute(ioBasic(t), std.os.argv[0], &real_path_buf) catch |err| switch (err) { + error.NetworkNotFound => unreachable, // Windows-only + else => |e| return e, + }; + if (real_path.len > out_buffer.len) + return error.NameTooLong; + const result = out_buffer[0..real_path.len]; + @memcpy(result, real_path); + return result.len; + } else if (argv0.len != 0) { + // argv[0] is not empty (and not a path): search it inside PATH + const PATH = posix.getenvZ("PATH") orelse return error.FileNotFound; + var path_it = std.mem.tokenizeScalar(u8, PATH, std.fs.path.delimiter); + while (path_it.next()) |a_path| { + var resolved_path_buf: [max_path_bytes - 1:0]u8 = undefined; + const resolved_path = std.fmt.bufPrintSentinel(&resolved_path_buf, "{s}/{s}", .{ + a_path, std.os.argv[0], + }, 0) catch continue; + + var real_path_buf: [max_path_bytes]u8 = undefined; + if (Io.Dir.realPathAbsolute(ioBasic(t), resolved_path, &real_path_buf)) |real_path| { + // found a file, and hope it is the right file + if (real_path.len > out_buffer.len) + return error.NameTooLong; + const result = out_buffer[0..real_path.len]; + @memcpy(result, real_path); + return result.len; + } else |_| continue; + } + } + return error.FileNotFound; + }, + .windows => { + const current_thread = Thread.getCurrent(t); + try current_thread.checkCancel(); + const w = windows; + const image_path_unicode_string = &w.peb().ProcessParameters.ImagePathName; + const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; + + // If ImagePathName is a symlink, then it will contain the path of the + // symlink, not the path that the symlink points to. We want the path + // that the symlink points to, though, so we need to get the realpath. + var path_name_w = try w.wToPrefixedFileW(null, image_path_name); + + const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; + const share_access = w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE; + const creation = w.FILE_OPEN; + const h_file = blk: { + const res = w.OpenFile(path_name_w.span(), .{ + .dir = null, + .access_mask = access_mask, + .share_access = share_access, + .creation = creation, + .filter = .any, + }) catch |err| switch (err) { + error.WouldBlock => unreachable, + else => |e| return e, + }; + break :blk res; + }; + defer w.CloseHandle(h_file); + + const wide_slice = w.GetFinalPathNameByHandle(h_file, .{}, out_buffer); + + const len = std.unicode.calcWtf8Len(wide_slice); + if (len > out_buffer.len) + return error.NameTooLong; + + const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice); + return end_index; + }, + else => @compileError("unsupported OS"), } } diff --git a/lib/std/debug/SelfInfo/Elf.zig b/lib/std/debug/SelfInfo/Elf.zig index 124768687c8e8026fa3411e4433f3b2119105aa0..213389bf04c2c592b41e9fa9b3020bf035de294a 100644 --- a/lib/std/debug/SelfInfo/Elf.zig +++ b/lib/std/debug/SelfInfo/Elf.zig @@ -329,7 +329,7 @@ const Module = struct { defer file.close(io); break :res std.debug.ElfFile.load(gpa, file, mod.build_id, &.native(mod.name)); } else res: { - const path = std.fs.selfExePathAlloc(gpa) catch |err| switch (err) { + const path = std.process.executablePathAlloc(io, gpa) catch |err| switch (err) { error.OutOfMemory => |e| return e, else => return error.ReadFailed, }; diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig index c7f9d8c352a012a00421ea1057cddcbeda2e3802..f0ac30cca22229ec8f26a07c3910876deb5e5676 100644 --- a/lib/std/debug/SelfInfo/Windows.zig +++ b/lib/std/debug/SelfInfo/Windows.zig @@ -434,7 +434,7 @@ const Module = struct { const pdb_file_open_result = if (fs.path.isAbsolute(path)) res: { break :res std.fs.cwd().openFile(io, path, .{}); } else res: { - const self_dir = fs.selfExeDirPathAlloc(gpa) catch |err| switch (err) { + const self_dir = std.process.executableDirPathAlloc(io, gpa) catch |err| switch (err) { error.OutOfMemory, error.Unexpected => |e| return e, else => return error.ReadFailed, }; diff --git a/lib/std/fs.zig b/lib/std/fs.zig index cb4daf7c50dc30a7eaf7488162985bfcf2ae39c1..78c924f8d915195844f414f1a81a45a1dd16f0c0 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -25,13 +25,6 @@ pub const File = std.Io.File; pub const path = @import("fs/path.zig"); pub const wasi = @import("fs/wasi.zig"); -// TODO audit these APIs with respect to Dir and absolute paths - -pub const realpath = posix.realpath; -pub const realpathZ = posix.realpathZ; -pub const realpathW = posix.realpathW; -pub const realpathW2 = posix.realpathW2; - pub const getAppDataDir = @import("fs/get_app_data_dir.zig").getAppDataDir; pub const GetAppDataDirError = @import("fs/get_app_data_dir.zig").GetAppDataDirError; @@ -241,15 +234,6 @@ pub fn deleteTreeAbsolute(io: Io, absolute_path: []const u8) !void { return dir.deleteTree(path.basename(absolute_path)); } -/// Same as `Dir.readLink`, except it asserts the path is absolute. -/// On Windows, `pathname` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `pathname` should be encoded as valid UTF-8. -/// On other platforms, `pathname` is an opaque sequence of bytes with no particular encoding. -pub fn readLinkAbsolute(pathname: []const u8, buffer: *[max_path_bytes]u8) ![]u8 { - assert(path.isAbsolute(pathname)); - return posix.readlink(pathname, buffer); -} - /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. @@ -287,222 +271,6 @@ pub fn symLinkAbsoluteW( return windows.CreateSymbolicLink(null, mem.span(sym_link_path_w), mem.span(target_path_w), flags.is_directory); } -// This is `posix.ReadLinkError || posix.RealPathError` with impossible errors excluded -pub const SelfExePathError = error{ - FileNotFound, - AccessDenied, - NameTooLong, - NotSupported, - NotDir, - SymLinkLoop, - InputOutput, - FileTooBig, - IsDir, - ProcessFdQuotaExceeded, - SystemFdQuotaExceeded, - NoDevice, - SystemResources, - NoSpaceLeft, - FileSystem, - BadPathName, - DeviceBusy, - SharingViolation, - PipeBusy, - NotLink, - PathAlreadyExists, - - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - ProcessNotFound, - - /// On Windows, antivirus software is enabled by default. It can be - /// disabled, but Windows Update sometimes ignores the user's preference - /// and re-enables it. When enabled, antivirus software on Windows - /// intercepts file system operations and makes them significantly slower - /// in addition to possibly failing with this error code. - AntivirusInterference, - - /// On Windows, the volume does not contain a recognized file system. File - /// system drivers might not be loaded, or the volume may be corrupt. - UnrecognizedVolume, - - Canceled, -} || posix.SysCtlError; - -/// `selfExePath` except allocates the result on the heap. -/// Caller owns returned memory. -pub fn selfExePathAlloc(allocator: Allocator) ![]u8 { - // Use of max_path_bytes here is justified as, at least on one tested Linux - // system, readlink will completely fail to return a result larger than - // PATH_MAX even if given a sufficiently large buffer. This makes it - // fundamentally impossible to get the selfExePath of a program running in - // a very deeply nested directory chain in this way. - // TODO(#4812): Investigate other systems and whether it is possible to get - // this path by trying larger and larger buffers until one succeeds. - var buf: [max_path_bytes]u8 = undefined; - return allocator.dupe(u8, try selfExePath(&buf)); -} - -/// Get the path to the current executable. Follows symlinks. -/// If you only need the directory, use selfExeDirPath. -/// If you only want an open file handle, use openSelfExe. -/// This function may return an error if the current executable -/// was deleted after spawning. -/// Returned value is a slice of out_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. -/// -/// On Linux, depends on procfs being mounted. If the currently executing binary has -/// been deleted, the file path looks something like `/a/b/c/exe (deleted)`. -/// TODO make the return type of this a null terminated pointer -pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { - if (is_darwin) { - // Note that _NSGetExecutablePath() will return "a path" to - // the executable not a "real path" to the executable. - var symlink_path_buf: [max_path_bytes:0]u8 = undefined; - var u32_len: u32 = max_path_bytes + 1; // include the sentinel - const rc = std.c._NSGetExecutablePath(&symlink_path_buf, &u32_len); - if (rc != 0) return error.NameTooLong; - - var real_path_buf: [max_path_bytes]u8 = undefined; - const real_path = std.posix.realpathZ(&symlink_path_buf, &real_path_buf) catch |err| switch (err) { - error.NetworkNotFound => unreachable, // Windows-only - else => |e| return e, - }; - if (real_path.len > out_buffer.len) return error.NameTooLong; - const result = out_buffer[0..real_path.len]; - @memcpy(result, real_path); - return result; - } - switch (native_os) { - .linux, .serenity => return posix.readlinkZ("/proc/self/exe", out_buffer) catch |err| switch (err) { - error.UnsupportedReparsePointType => unreachable, // Windows-only - error.NetworkNotFound => unreachable, // Windows-only - else => |e| return e, - }, - .illumos => return posix.readlinkZ("/proc/self/path/a.out", out_buffer) catch |err| switch (err) { - error.UnsupportedReparsePointType => unreachable, // Windows-only - error.NetworkNotFound => unreachable, // Windows-only - else => |e| return e, - }, - .freebsd, .dragonfly => { - var mib = [4]c_int{ posix.CTL.KERN, posix.KERN.PROC, posix.KERN.PROC_PATHNAME, -1 }; - var out_len: usize = out_buffer.len; - try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0); - // TODO could this slice from 0 to out_len instead? - return mem.sliceTo(out_buffer, 0); - }, - .netbsd => { - var mib = [4]c_int{ posix.CTL.KERN, posix.KERN.PROC_ARGS, -1, posix.KERN.PROC_PATHNAME }; - var out_len: usize = out_buffer.len; - try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0); - // TODO could this slice from 0 to out_len instead? - return mem.sliceTo(out_buffer, 0); - }, - .openbsd, .haiku => { - // OpenBSD doesn't support getting the path of a running process, so try to guess it - if (std.os.argv.len == 0) - return error.FileNotFound; - - const argv0 = mem.span(std.os.argv[0]); - if (mem.find(u8, argv0, "/") != null) { - // argv[0] is a path (relative or absolute): use realpath(3) directly - var real_path_buf: [max_path_bytes]u8 = undefined; - const real_path = posix.realpathZ(std.os.argv[0], &real_path_buf) catch |err| switch (err) { - error.NetworkNotFound => unreachable, // Windows-only - else => |e| return e, - }; - if (real_path.len > out_buffer.len) - return error.NameTooLong; - const result = out_buffer[0..real_path.len]; - @memcpy(result, real_path); - return result; - } else if (argv0.len != 0) { - // argv[0] is not empty (and not a path): search it inside PATH - const PATH = posix.getenvZ("PATH") orelse return error.FileNotFound; - var path_it = mem.tokenizeScalar(u8, PATH, path.delimiter); - while (path_it.next()) |a_path| { - var resolved_path_buf: [max_path_bytes - 1:0]u8 = undefined; - const resolved_path = std.fmt.bufPrintSentinel(&resolved_path_buf, "{s}/{s}", .{ - a_path, - std.os.argv[0], - }, 0) catch continue; - - var real_path_buf: [max_path_bytes]u8 = undefined; - if (posix.realpathZ(resolved_path, &real_path_buf)) |real_path| { - // found a file, and hope it is the right file - if (real_path.len > out_buffer.len) - return error.NameTooLong; - const result = out_buffer[0..real_path.len]; - @memcpy(result, real_path); - return result; - } else |_| continue; - } - } - return error.FileNotFound; - }, - .windows => { - const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; - const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; - - // If ImagePathName is a symlink, then it will contain the path of the - // symlink, not the path that the symlink points to. We want the path - // that the symlink points to, though, so we need to get the realpath. - var pathname_w = try windows.wToPrefixedFileW(null, image_path_name); - - const wide_slice = try std.fs.cwd().realpathW2(pathname_w.span(), &pathname_w.data); - - const len = std.unicode.calcWtf8Len(wide_slice); - if (len > out_buffer.len) - return error.NameTooLong; - - const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice); - return out_buffer[0..end_index]; - }, - else => @compileError("std.fs.selfExePath not supported for this target"), - } -} - -/// `selfExeDirPath` except allocates the result on the heap. -/// Caller owns returned memory. -pub fn selfExeDirPathAlloc(allocator: Allocator) ![]u8 { - // Use of max_path_bytes here is justified as, at least on one tested Linux - // system, readlink will completely fail to return a result larger than - // PATH_MAX even if given a sufficiently large buffer. This makes it - // fundamentally impossible to get the selfExeDirPath of a program running - // in a very deeply nested directory chain in this way. - // TODO(#4812): Investigate other systems and whether it is possible to get - // this path by trying larger and larger buffers until one succeeds. - var buf: [max_path_bytes]u8 = undefined; - return allocator.dupe(u8, try selfExeDirPath(&buf)); -} - -/// Get the directory path that contains the current executable. -/// Returned value is a slice of out_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 selfExeDirPath(out_buffer: []u8) SelfExePathError![]const u8 { - const self_exe_path = try selfExePath(out_buffer); - // Assume that the OS APIs return absolute paths, and therefore dirname - // will not return null. - return path.dirname(self_exe_path).?; -} - -/// `realpath`, except caller must free the returned memory. -/// 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. -/// See also `Dir.realpath`. -pub fn realpathAlloc(allocator: Allocator, pathname: []const u8) ![]u8 { - // Use of max_path_bytes here is valid as the realpath function does not - // have a variant that takes an arbitrary-size buffer. - // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 - // NULL out parameter (GNU's canonicalize_file_name) to handle overelong - // paths. musl supports passing NULL but restricts the output to PATH_MAX - // anyway. - var buf: [max_path_bytes]u8 = undefined; - return allocator.dupe(u8, try posix.realpath(pathname, &buf)); -} - test { _ = AtomicFile; _ = Dir; diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 7d566da0e95ec6d1f3a70eb9c10e877da8347421..1a600fb82cc5f4fe436f5a3d3ab5f79c5874b02c 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -4,6 +4,7 @@ const native_os = builtin.os.tag; const std = @import("../std.zig"); const Io = std.Io; const testing = std.testing; +const expect = std.testing.expect; const fs = std.fs; const mem = std.mem; const wasi = std.os.wasi; @@ -1177,21 +1178,22 @@ test "renameAbsolute" { dir.close(io); } -test "openSelfExe" { +test "openExecutable" { if (native_os == .wasi) return error.SkipZigTest; const io = testing.io; - const self_exe_file = try std.fs.openSelfExe(.{}); + const self_exe_file = try std.fs.openExecutable(.{}); self_exe_file.close(io); } -test "selfExePath" { +test "executablePath" { if (native_os == .wasi) return error.SkipZigTest; + const io = testing.io; var buf: [fs.max_path_bytes]u8 = undefined; - const buf_self_exe_path = try std.fs.selfExePath(&buf); - const alloc_self_exe_path = try std.fs.selfExePathAlloc(testing.allocator); + const buf_self_exe_path = try std.process.executablePath(io, &buf); + const alloc_self_exe_path = try std.process.executablePathAlloc(io, testing.allocator); defer testing.allocator.free(alloc_self_exe_path); try testing.expectEqualSlices(u8, buf_self_exe_path, alloc_self_exe_path); } @@ -2371,3 +2373,46 @@ test "File.Writer sendfile with buffered contents" { try testing.expectEqualStrings("abcd", try check_r.interface.take(4)); try testing.expectError(error.EndOfStream, check_r.interface.takeByte()); } + +test "readlink on Windows" { + if (native_os != .windows) return error.SkipZigTest; + + try testReadlink("C:\\ProgramData", "C:\\Users\\All Users"); + try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User"); + try testReadlink("C:\\Users", "C:\\Documents and Settings"); +} + +fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { + var buffer: [fs.max_path_bytes]u8 = undefined; + const given = try Dir.readLinkAbsolute(symlink_path, buffer[0..]); + try expect(mem.eql(u8, target_path, given)); +} + +test "readlinkat" { + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + // create file + try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" }); + + // create a symbolic link + if (native_os == .windows) { + std.os.windows.CreateSymbolicLink( + tmp.dir.fd, + &[_]u16{ 'l', 'i', 'n', 'k' }, + &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, + false, + ) catch |err| switch (err) { + // Symlink requires admin privileges on windows, so this test can legitimately fail. + error.AccessDenied => return error.SkipZigTest, + else => return err, + }; + } else { + try posix.symlinkat("file.txt", tmp.dir.fd, "link"); + } + + // read the link + var buffer: [fs.max_path_bytes]u8 = undefined; + const read_link = try tmp.dir.readLink("link", &buffer); + try expect(mem.eql(u8, "file.txt", read_link)); +} diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 19313e3ff79031c2113f86ed7be304ec95b6a93b..0071a72a26dfd5d301666b18e890c84e3a1df79f 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -111,20 +111,6 @@ test "open smoke test" { } } -test "readlink on Windows" { - if (native_os != .windows) return error.SkipZigTest; - - try testReadlink("C:\\ProgramData", "C:\\Users\\All Users"); - try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User"); - try testReadlink("C:\\Users", "C:\\Documents and Settings"); -} - -fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { - var buffer: [fs.max_path_bytes]u8 = undefined; - const given = try posix.readlink(symlink_path, buffer[0..]); - try expect(mem.eql(u8, target_path, given)); -} - fn getLinkInfo(fd: posix.fd_t) !struct { posix.ino_t, posix.nlink_t } { if (native_os == .linux) { const stx = try linux.wrapped.statx( @@ -216,35 +202,6 @@ test "fstatat" { // try expectEqual(stat.blocks, statat.blocks); } -test "readlinkat" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - // create file - try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" }); - - // create a symbolic link - if (native_os == .windows) { - std.os.windows.CreateSymbolicLink( - tmp.dir.fd, - &[_]u16{ 'l', 'i', 'n', 'k' }, - &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, - false, - ) catch |err| switch (err) { - // Symlink requires admin privileges on windows, so this test can legitimately fail. - error.AccessDenied => return error.SkipZigTest, - else => return err, - }; - } else { - try posix.symlinkat("file.txt", tmp.dir.fd, "link"); - } - - // read the link - var buffer: [fs.max_path_bytes]u8 = undefined; - const read_link = try posix.readlinkat(tmp.dir.fd, "link", buffer[0..]); - try expect(mem.eql(u8, "file.txt", read_link)); -} - test "getrandom" { var buf_a: [50]u8 = undefined; var buf_b: [50]u8 = undefined; diff --git a/lib/std/process.zig b/lib/std/process.zig index a8dede6ad4aa52ec5750eefef3cf7ee5d32bc8f7..77f91587fef16fd3680c6ec5a6c653b50282e6f7 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -3,6 +3,7 @@ const native_os = builtin.os.tag; const std = @import("std.zig"); const Io = std.Io; +const File = std.Io.File; const fs = std.fs; const mem = std.mem; const math = std.math; @@ -12,6 +13,7 @@ const testing = std.testing; const posix = std.posix; const windows = std.os.windows; const unicode = std.unicode; +const max_path_bytes = std.fs.max_path_bytes; pub const Child = @import("process/Child.zig"); pub const abort = posix.abort; @@ -37,7 +39,7 @@ pub const GetCwdAllocError = Allocator.Error || error{CurrentWorkingDirectoryUnl pub fn getCwdAlloc(allocator: Allocator) GetCwdAllocError![]u8 { // The use of max_path_bytes here is just a heuristic: most paths will fit // in stack_buf, avoiding an extra allocation in the common case. - var stack_buf: [fs.max_path_bytes]u8 = undefined; + var stack_buf: [max_path_bytes]u8 = undefined; var heap_buf: ?[]u8 = null; defer if (heap_buf) |buf| allocator.free(buf); @@ -2112,3 +2114,97 @@ pub fn fatal(comptime format: []const u8, format_arguments: anytype) noreturn { std.log.err(format, format_arguments); exit(1); } + +pub const ExecutablePathBaseError = error{ + FileNotFound, + AccessDenied, + NotSupported, + NotDir, + SymLinkLoop, + InputOutput, + FileTooBig, + IsDir, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + NoDevice, + SystemResources, + NoSpaceLeft, + FileSystem, + BadPathName, + DeviceBusy, + SharingViolation, + PipeBusy, + NotLink, + PathAlreadyExists, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, + ProcessNotFound, + /// On Windows, antivirus software is enabled by default. It can be + /// disabled, but Windows Update sometimes ignores the user's preference + /// and re-enables it. When enabled, antivirus software on Windows + /// intercepts file system operations and makes them significantly slower + /// in addition to possibly failing with this error code. + AntivirusInterference, + /// On Windows, the volume does not contain a recognized file system. File + /// system drivers might not be loaded, or the volume may be corrupt. + UnrecognizedVolume, + PermissionDenied, +} || Io.Cancelable || Io.UnexpectedError; + +pub const ExecutablePathAllocError = ExecutablePathBaseError || Allocator.Error; + +pub fn executablePathAlloc(io: Io, allocator: Allocator) ExecutablePathAllocError![:0]u8 { + var buffer: [max_path_bytes]u8 = undefined; + const n = executablePath(io, &buffer) catch |err| switch (err) { + error.NameTooLong => unreachable, + else => |e| return e, + }; + return allocator.dupeZ(u8, buffer[0..n]); +} + +pub const ExecutablePathError = ExecutablePathBaseError || error{NameTooLong}; + +/// Get the path to the current executable, following symlinks. +/// +/// This function may return an error if the current executable +/// was deleted after spawning. +/// +/// Returned value is a slice of out_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. +/// +/// On Linux, depends on procfs being mounted. If the currently executing binary has +/// been deleted, the file path looks something like "/a/b/c/exe (deleted)". +/// +/// See also: +/// * `executableDirPath` - to obtain only the directory +/// * `openExecutable` - to obtain only an open file handle +pub fn executablePath(io: Io, out_buffer: []u8) ExecutablePathError!usize { + return io.vtable.processExecutablePath(io.userdata, out_buffer); +} + +/// Get the directory path that contains the current executable. +/// +/// Returns index into `out_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 executableDirPath(out_buffer: []u8) ExecutablePathError!usize { + const n = try executablePath(out_buffer); + // Assert that the OS APIs return absolute paths, and therefore dirname + // will not return null. + return std.fs.path.dirname(out_buffer[0..n]).?; +} + +/// Same as `executableDirPath` except allocates the result. +pub fn executableDirPathAlloc(allocator: Allocator) ![]u8 { + var buffer: [max_path_bytes]u8 = undefined; + return allocator.dupe(u8, try executableDirPath(&buffer)); +} + +pub const OpenExecutableError = File.OpenError || ExecutablePathError || File.LockError; + +pub fn openExecutable(io: Io, flags: File.OpenFlags) OpenExecutableError!File { + return io.vtable.processExecutableOpen(io.userdata, flags); +} diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index cc74da956af50b3a3b45f5327d29d437e5544c8d..5c110a576d5d54871c02a956fbd1082e291681d9 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -209,7 +209,6 @@ pub const DetectError = error{ DeviceBusy, OSVersionDetectionFail, Unexpected, - ProcessNotFound, } || Io.Cancelable; /// 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 { error.SocketUnconnected => return error.Unexpected, error.AccessDenied, - error.ProcessNotFound, error.SymLinkLoop, error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, @@ -553,7 +551,6 @@ pub const AbiAndDynamicLinkerFromFileError = error{ SystemResources, ProcessFdQuotaExceeded, SystemFdQuotaExceeded, - ProcessNotFound, IsDir, WouldBlock, InputOutput, @@ -693,8 +690,10 @@ fn abiAndDynamicLinkerFromFile( // So far, no luck. Next we try to see if the information is // present in the symlink data for the dynamic linker path. - var link_buf: [posix.PATH_MAX]u8 = undefined; - const link_name = posix.readlink(dl_path, &link_buf) catch |err| switch (err) { + var link_buffer: [posix.PATH_MAX]u8 = undefined; + const link_name = if (Io.Dir.readLinkAbsolute(io, dl_path, &link_buffer)) |n| + link_buffer[0..n] + else |err| switch (err) { error.NameTooLong => unreachable, error.BadPathName => unreachable, // Windows only error.UnsupportedReparsePointType => unreachable, // Windows only @@ -839,7 +838,6 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { error.NotDir => return error.GLibCNotFound, error.IsDir => return error.GLibCNotFound, - error.ProcessNotFound => |e| return e, error.ProcessFdQuotaExceeded => |e| return e, error.SystemFdQuotaExceeded => |e| return e, error.SystemResources => |e| return e, @@ -1103,7 +1101,6 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ error.SymLinkLoop, error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, - error.ProcessNotFound, error.Canceled, => |e| return e,