Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Sources/NIOFS/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,37 @@ public struct FileSystem: Sendable, FileSystemProtocol {
}
}

/// Returns the path of the current user's home directory.
///
/// #### Implementation details
///
/// This function first checks the `HOME` environment variable (and `USERPROFILE` on Windows).
/// If not set, on Darwin/Linux/Android it uses `getpwuid_r(3)` to query the password database.
///
/// Note: `getpwuid_r` can potentially block on I/O (e.g., when using NIS or LDAP),
/// which is why this property is async when falling back to the password database.
///
/// - Returns: The path to the current user's home directory.
public var homeDirectory: NIOFilePath {
get async throws {
if let path = Libc.homeDirectoryFromEnvironment() {
return NIOFilePath(path)
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic)
return try await self.threadPool.runIfActive {
NIOFilePath(
try Libc.homeDirectoryFromPasswd().mapError { errno in
FileSystemError.getpwuid_r(errno: errno, location: .here())
}.get()
)
}
#else
throw FileSystemError.getpwuid_r(errno: .noSuchFileOrDirectory, location: .here())
#endif
}
}

/// Returns a path to a temporary directory.
///
/// #### Implementation details
Expand Down
11 changes: 11 additions & 0 deletions Sources/NIOFS/FileSystemError+Syscall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,17 @@ extension FileSystemError {
)
}

@_spi(Testing)
public static func getpwuid_r(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 fcopyfile(
errno: Errno,
Expand Down
3 changes: 3 additions & 0 deletions Sources/NIOFS/FileSystemProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
50 changes: 50 additions & 0 deletions Sources/NIOFS/Internal/System Calls/Syscall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,13 @@ public enum Syscall: Sendable {
system_futimens(fd.rawValue, times)
}
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic)
@_spi(Testing)
public static func getuid() -> uid_t {
system_getuid()
}
#endif
}

@_spi(Testing)
Expand Down Expand Up @@ -437,4 +444,47 @@ public enum Libc: Sendable {
libc_fts_close(pointer)
}
}

@_spi(Testing)
public static func homeDirectoryFromEnvironment() -> FilePath? {
if let home = getenv("HOME"), home.pointee != 0 {
return FilePath(String(cString: home))
}
#if os(Windows)
if let profile = getenv("USERPROFILE"), profile.pointee != 0 {
return FilePath(String(cString: profile))
}
#endif
return nil
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic)
@_spi(Testing)
public static func homeDirectoryFromPasswd() -> Result<FilePath, Errno> {
let uid = Syscall.getuid()
var pwd = passwd()
var result: UnsafeMutablePointer<passwd>? = nil

return withUnsafeTemporaryAllocation(of: CChar.self, capacity: 1024) { buffer in
let callResult = nothingOrErrno(retryOnInterrupt: true) {
libc_getpwuid_r(
uid,
&pwd,
buffer.baseAddress!,
buffer.count,
&result
)
}
switch callResult {
case .success:
guard result != nil, let directoryPointer = pwd.pw_dir else {
return .failure(.noSuchFileOrDirectory)
}
return .success(FilePath(String(cString: directoryPointer)))
case .failure(let errno):
return .failure(errno)
}
}
}
#endif
}
18 changes: 18 additions & 0 deletions Sources/NIOFS/Internal/System Calls/Syscalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,21 @@ internal func libc_fts_close(
) -> CInt {
fts_close(fts)
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Android)
/// getuid(2): Get user identification
internal func system_getuid() -> uid_t {
getuid()
}

/// getpwuid_r(3): Get password file entry
internal func libc_getpwuid_r(
_ uid: uid_t,
_ pwd: UnsafeMutablePointer<passwd>,
_ buffer: UnsafeMutablePointer<CChar>,
_ bufferSize: Int,
_ result: UnsafeMutablePointer<UnsafeMutablePointer<passwd>?>
) -> CInt {
getpwuid_r(uid, pwd, buffer, bufferSize, result)
}
#endif
29 changes: 29 additions & 0 deletions Sources/_NIOFileSystem/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,35 @@ public struct FileSystem: Sendable, FileSystemProtocol {
}
}

/// Returns the path of the current user's home directory.
///
/// #### Implementation details
///
/// This function first checks the `HOME` environment variable (and `USERPROFILE` on Windows).
/// If not set, on Darwin/Linux/Android it uses `getpwuid_r(3)` to query the password database.
///
/// Note: `getpwuid_r` can potentially block on I/O (e.g., when using NIS or LDAP),
/// which is why this property is async when falling back to the password database.
///
/// - Returns: The path to the current user's home directory.
public var homeDirectory: FilePath {
get async throws {
if let path = Libc.homeDirectoryFromEnvironment() {
return path
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Android)
return try await self.threadPool.runIfActive {
try Libc.homeDirectoryFromPasswd().mapError { errno in
FileSystemError.getpwuid_r(errno: errno, location: .here())
}.get()
}
#else
throw FileSystemError.getpwuid_r(errno: .noSuchFileOrDirectory, location: .here())
#endif
}
}

/// Returns a path to a temporary directory.
///
/// #### Implementation details
Expand Down
11 changes: 11 additions & 0 deletions Sources/_NIOFileSystem/FileSystemError+Syscall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,17 @@ extension FileSystemError {
)
}

@_spi(Testing)
public static func getpwuid_r(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 fcopyfile(
errno: Errno,
Expand Down
3 changes: 3 additions & 0 deletions Sources/_NIOFileSystem/FileSystemProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
50 changes: 50 additions & 0 deletions Sources/_NIOFileSystem/Internal/System Calls/Syscall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,13 @@ public enum Syscall: Sendable {
system_futimens(fd.rawValue, times)
}
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic)
@_spi(Testing)
public static func getuid() -> uid_t {
system_getuid()
}
#endif
}

@_spi(Testing)
Expand Down Expand Up @@ -437,4 +444,47 @@ public enum Libc: Sendable {
libc_fts_close(pointer)
}
}

@_spi(Testing)
public static func homeDirectoryFromEnvironment() -> FilePath? {
if let home = getenv("HOME"), home.pointee != 0 {
return FilePath(String(cString: home))
}
#if os(Windows)
if let profile = getenv("USERPROFILE"), profile.pointee != 0 {
return FilePath(String(cString: profile))
}
#endif
return nil
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic)
@_spi(Testing)
public static func homeDirectoryFromPasswd() -> Result<FilePath, Errno> {
let uid = Syscall.getuid()
var pwd = passwd()
var result: UnsafeMutablePointer<passwd>? = nil

return withUnsafeTemporaryAllocation(of: CChar.self, capacity: 1024) { buffer in
let callResult = nothingOrErrno(retryOnInterrupt: true) {
libc_getpwuid_r(
uid,
&pwd,
buffer.baseAddress!,
buffer.count,
&result
)
}
switch callResult {
case .success:
guard result != nil, let directoryPointer = pwd.pw_dir else {
return .failure(.noSuchFileOrDirectory)
}
return .success(FilePath(String(cString: directoryPointer)))
case .failure(let errno):
return .failure(errno)
}
}
}
#endif
}
18 changes: 18 additions & 0 deletions Sources/_NIOFileSystem/Internal/System Calls/Syscalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,21 @@ internal func libc_fts_close(
) -> CInt {
fts_close(fts)
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Android)
/// getuid(2): Get user identification
internal func system_getuid() -> uid_t {
getuid()
}

/// getpwuid_r(3): Get password file entry
internal func libc_getpwuid_r(
_ uid: uid_t,
_ pwd: UnsafeMutablePointer<passwd>,
_ buffer: UnsafeMutablePointer<CChar>,
_ bufferSize: Int,
_ result: UnsafeMutablePointer<UnsafeMutablePointer<passwd>?>
) -> CInt {
getpwuid_r(uid, pwd, buffer, bufferSize, result)
}
#endif
43 changes: 43 additions & 0 deletions Tests/NIOFSIntegrationTests/FileSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,49 @@ 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 testHomeDirectoryFromEnvironment() async throws {
// Should return a value when HOME is set (which it typically is)
if let path = Libc.homeDirectoryFromEnvironment() {
XCTAssert(!path.isEmpty)
XCTAssert(path.isAbsolute)

// Verify it matches the high-level API
let fsHome = try await self.fs.homeDirectory
XCTAssertEqual(path, fsHome.underlying)
} else {
// If it returns nil, then HOME check should fail
let home = getenv("HOME")
XCTAssertTrue(home == nil || home!.pointee == 0, "Expected HOME to be unset or empty")

#if os(Windows)
let profile = getenv("USERPROFILE")
XCTAssertTrue(profile == nil || profile!.pointee == 0, "Expected USERPROFILE to be unset or empty")
#endif
}
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Android)
func testHomeDirectoryFromPasswd() {
// Should always succeed on Unix-like systems for the current user
let result = Libc.homeDirectoryFromPasswd()
switch result {
case .success(let path):
XCTAssert(!path.isEmpty)
XCTAssert(path.isAbsolute)
case .failure(let errno):
XCTFail("Expected success, got error: \(errno)")
}
}
#endif

func testInfo() async throws {
let info = try await self.fs.info(forFileAt: .testDataReadme, infoAboutSymbolicLink: false)
XCTAssertEqual(info?.type, .regular)
Expand Down
Loading