diff --git a/Sources/NIOFS/Docs.docc/Extensions/FileSystemProtocol.md b/Sources/NIOFS/Docs.docc/Extensions/FileSystemProtocol.md index cdfc193e9e..3507b17d52 100644 --- a/Sources/NIOFS/Docs.docc/Extensions/FileSystemProtocol.md +++ b/Sources/NIOFS/Docs.docc/Extensions/FileSystemProtocol.md @@ -42,5 +42,6 @@ closing it to avoid leaking resources. ### System directories - ``currentWorkingDirectory`` +- ``homeDirectory`` - ``temporaryDirectory`` - ``withTemporaryDirectory(prefix:options:execute:)`` diff --git a/Sources/NIOFS/FileSystem.swift b/Sources/NIOFS/FileSystem.swift index b54e6cb28b..05c2c0c2f0 100644 --- a/Sources/NIOFS/FileSystem.swift +++ b/Sources/NIOFS/FileSystem.swift @@ -636,6 +636,18 @@ public struct FileSystem: Sendable, FileSystemProtocol { } } + /// Returns the path of the current user's home directory. + public var homeDirectory: NIOFilePath { + get async throws { + let result = try await self.threadPool.runIfActive { + try Libc.homeDirectoryForCurrentUser().mapError { errno in + FileSystemError.homeDirectory(errno: errno, location: .here()) + }.get() + } + return .init(result) + } + } + /// Returns a path to a temporary directory. /// /// #### Implementation details diff --git a/Sources/NIOFS/FileSystemError+Syscall.swift b/Sources/NIOFS/FileSystemError+Syscall.swift index 280041e5c6..53ffc42c61 100644 --- a/Sources/NIOFS/FileSystemError+Syscall.swift +++ b/Sources/NIOFS/FileSystemError+Syscall.swift @@ -974,6 +974,17 @@ extension FileSystemError { ) } + @_spi(Testing) + public static func homeDirectory(errno: Errno, location: SourceLocation) -> Self { + FileSystemError( + code: .unavailable, + message: "Can't get home directory for current user.", + systemCall: "getpwuid_r", + errno: errno, + location: location + ) + } + @_spi(Testing) public static func confstr(name: String, errno: Errno, location: SourceLocation) -> Self { FileSystemError( diff --git a/Sources/NIOFS/FileSystemProtocol.swift b/Sources/NIOFS/FileSystemProtocol.swift index 4b98032acc..8fb4b283ab 100644 --- a/Sources/NIOFS/FileSystemProtocol.swift +++ b/Sources/NIOFS/FileSystemProtocol.swift @@ -108,6 +108,9 @@ public protocol FileSystemProtocol: Sendable { /// Returns the current working directory. var currentWorkingDirectory: NIOFilePath { get async throws } + /// Returns the current user's home directory. + var homeDirectory: NIOFilePath { get async throws } + /// Returns the path of the temporary directory. var temporaryDirectory: NIOFilePath { get async throws } diff --git a/Sources/NIOFS/Internal/System Calls/Syscall.swift b/Sources/NIOFS/Internal/System Calls/Syscall.swift index c3965c0f5d..c76f19dd8f 100644 --- a/Sources/NIOFS/Internal/System Calls/Syscall.swift +++ b/Sources/NIOFS/Internal/System Calls/Syscall.swift @@ -362,6 +362,67 @@ public enum Libc: Sendable { } } + static func homeDirectoryForCurrentUser() -> Result { + if let home = getenv("HOME"), home.pointee != 0 { + return .success(FilePath(String(cString: home))) + } + + #if os(Windows) + if let profile = getenv("USERPROFILE"), profile.pointee != 0 { + return .success(FilePath(String(cString: profile))) + } + return .failure(.noSuchFileOrDirectory) + #else + return self._homeDirectoryFromPasswordDatabase() + #endif + } + + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Android) + private static func _homeDirectoryFromPasswordDatabase() -> Result { + var pwd = passwd() + var result: UnsafeMutablePointer? = nil + var buffer = [CChar](repeating: 0, count: 256) + + let uid: uid_t = { + #if canImport(Darwin) + return Darwin.getuid() + #elseif canImport(Glibc) + return Glibc.getuid() + #elseif canImport(Musl) + return Musl.getuid() + #else + return 0 + #endif + }() + + while true { + let error: CInt = buffer.withUnsafeMutableBufferPointer { pointer in + guard let baseAddress = pointer.baseAddress else { + return CInt(ERANGE) + } + + return withUnsafeMutablePointer(to: &result) { resultPointer in + libc_getpwuid_r(uid, &pwd, baseAddress, pointer.count, resultPointer) + } + } + + if error == 0 { + guard result != nil, let directoryPointer = pwd.pw_dir else { + return .failure(.noSuchFileOrDirectory) + } + return .success(FilePath(String(cString: directoryPointer))) + } + + if error == ERANGE { + buffer.append(contentsOf: repeatElement(0, count: buffer.count)) + continue + } + + return .failure(Errno(rawValue: error)) + } + } + #endif + #if !os(Android) static func constr(_ name: CInt) -> Result { var buffer = [CInterop.PlatformChar](repeating: 0, count: 128) diff --git a/Sources/NIOFS/Internal/System Calls/Syscalls.swift b/Sources/NIOFS/Internal/System Calls/Syscalls.swift index a871876025..7c888b187b 100644 --- a/Sources/NIOFS/Internal/System Calls/Syscalls.swift +++ b/Sources/NIOFS/Internal/System Calls/Syscalls.swift @@ -440,6 +440,17 @@ internal func libc_getcwd( getcwd(buffer, size) } +/// getpwuid_r(3): Get password file entry +internal func libc_getpwuid_r( + _ uid: uid_t, + _ pwd: UnsafeMutablePointer, + _ buffer: UnsafeMutablePointer, + _ bufferSize: Int, + _ result: UnsafeMutablePointer?> +) -> CInt { + getpwuid_r(uid, pwd, buffer, bufferSize, result) +} + /// confstr(3) #if !os(Android) internal func libc_confstr( diff --git a/Sources/_NIOFileSystem/FileSystem.swift b/Sources/_NIOFileSystem/FileSystem.swift index 8b7df2bb1b..63226eaacf 100644 --- a/Sources/_NIOFileSystem/FileSystem.swift +++ b/Sources/_NIOFileSystem/FileSystem.swift @@ -636,6 +636,17 @@ public struct FileSystem: Sendable, FileSystemProtocol { } } + /// Returns the path of the current user's home directory. + public var homeDirectory: FilePath { + get async throws { + try await self.threadPool.runIfActive { + try Libc.homeDirectoryForCurrentUser().mapError { errno in + FileSystemError.homeDirectory(errno: errno, location: .here()) + }.get() + } + } + } + /// Returns a path to a temporary directory. /// /// #### Implementation details diff --git a/Sources/_NIOFileSystem/FileSystemError+Syscall.swift b/Sources/_NIOFileSystem/FileSystemError+Syscall.swift index 280041e5c6..53ffc42c61 100644 --- a/Sources/_NIOFileSystem/FileSystemError+Syscall.swift +++ b/Sources/_NIOFileSystem/FileSystemError+Syscall.swift @@ -974,6 +974,17 @@ extension FileSystemError { ) } + @_spi(Testing) + public static func homeDirectory(errno: Errno, location: SourceLocation) -> Self { + FileSystemError( + code: .unavailable, + message: "Can't get home directory for current user.", + systemCall: "getpwuid_r", + errno: errno, + location: location + ) + } + @_spi(Testing) public static func confstr(name: String, errno: Errno, location: SourceLocation) -> Self { FileSystemError( diff --git a/Sources/_NIOFileSystem/FileSystemProtocol.swift b/Sources/_NIOFileSystem/FileSystemProtocol.swift index 2c7844e038..7b3f771c2b 100644 --- a/Sources/_NIOFileSystem/FileSystemProtocol.swift +++ b/Sources/_NIOFileSystem/FileSystemProtocol.swift @@ -108,6 +108,9 @@ public protocol FileSystemProtocol: Sendable { /// Returns the current working directory. var currentWorkingDirectory: FilePath { get async throws } + /// Returns the current user's home directory. + var homeDirectory: FilePath { get async throws } + /// Returns the path of the temporary directory. var temporaryDirectory: FilePath { get async throws } diff --git a/Sources/_NIOFileSystem/Internal/System Calls/Syscall.swift b/Sources/_NIOFileSystem/Internal/System Calls/Syscall.swift index c3965c0f5d..c76f19dd8f 100644 --- a/Sources/_NIOFileSystem/Internal/System Calls/Syscall.swift +++ b/Sources/_NIOFileSystem/Internal/System Calls/Syscall.swift @@ -362,6 +362,67 @@ public enum Libc: Sendable { } } + static func homeDirectoryForCurrentUser() -> Result { + if let home = getenv("HOME"), home.pointee != 0 { + return .success(FilePath(String(cString: home))) + } + + #if os(Windows) + if let profile = getenv("USERPROFILE"), profile.pointee != 0 { + return .success(FilePath(String(cString: profile))) + } + return .failure(.noSuchFileOrDirectory) + #else + return self._homeDirectoryFromPasswordDatabase() + #endif + } + + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Android) + private static func _homeDirectoryFromPasswordDatabase() -> Result { + var pwd = passwd() + var result: UnsafeMutablePointer? = nil + var buffer = [CChar](repeating: 0, count: 256) + + let uid: uid_t = { + #if canImport(Darwin) + return Darwin.getuid() + #elseif canImport(Glibc) + return Glibc.getuid() + #elseif canImport(Musl) + return Musl.getuid() + #else + return 0 + #endif + }() + + while true { + let error: CInt = buffer.withUnsafeMutableBufferPointer { pointer in + guard let baseAddress = pointer.baseAddress else { + return CInt(ERANGE) + } + + return withUnsafeMutablePointer(to: &result) { resultPointer in + libc_getpwuid_r(uid, &pwd, baseAddress, pointer.count, resultPointer) + } + } + + if error == 0 { + guard result != nil, let directoryPointer = pwd.pw_dir else { + return .failure(.noSuchFileOrDirectory) + } + return .success(FilePath(String(cString: directoryPointer))) + } + + if error == ERANGE { + buffer.append(contentsOf: repeatElement(0, count: buffer.count)) + continue + } + + return .failure(Errno(rawValue: error)) + } + } + #endif + #if !os(Android) static func constr(_ name: CInt) -> Result { var buffer = [CInterop.PlatformChar](repeating: 0, count: 128) diff --git a/Sources/_NIOFileSystem/Internal/System Calls/Syscalls.swift b/Sources/_NIOFileSystem/Internal/System Calls/Syscalls.swift index a871876025..7c888b187b 100644 --- a/Sources/_NIOFileSystem/Internal/System Calls/Syscalls.swift +++ b/Sources/_NIOFileSystem/Internal/System Calls/Syscalls.swift @@ -440,6 +440,17 @@ internal func libc_getcwd( getcwd(buffer, size) } +/// getpwuid_r(3): Get password file entry +internal func libc_getpwuid_r( + _ uid: uid_t, + _ pwd: UnsafeMutablePointer, + _ buffer: UnsafeMutablePointer, + _ bufferSize: Int, + _ result: UnsafeMutablePointer?> +) -> CInt { + getpwuid_r(uid, pwd, buffer, bufferSize, result) +} + /// confstr(3) #if !os(Android) internal func libc_confstr( diff --git a/Tests/NIOFSIntegrationTests/FileSystemTests.swift b/Tests/NIOFSIntegrationTests/FileSystemTests.swift index cfc0820d19..4b76c8c490 100644 --- a/Tests/NIOFSIntegrationTests/FileSystemTests.swift +++ b/Tests/NIOFSIntegrationTests/FileSystemTests.swift @@ -511,6 +511,15 @@ final class FileSystemTests: XCTestCase { XCTAssert(directory.underlying.isAbsolute) } + func testHomeDirectory() async throws { + let directory = try await self.fs.homeDirectory + XCTAssert(!directory.underlying.isEmpty) + XCTAssert(directory.underlying.isAbsolute) + + let info = try await self.fs.info(forFileAt: directory, infoAboutSymbolicLink: false) + XCTAssertEqual(info?.type, .directory) + } + func testTemporaryDirectory() async throws { let directory = try await self.fs.temporaryDirectory XCTAssert(!directory.underlying.isEmpty)