Skip to content
Open
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
1 change: 1 addition & 0 deletions Sources/NIOFS/Docs.docc/Extensions/FileSystemProtocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ closing it to avoid leaking resources.
### System directories

- ``currentWorkingDirectory``
- ``homeDirectory``
- ``temporaryDirectory``
- ``withTemporaryDirectory(prefix:options:execute:)``
12 changes: 12 additions & 0 deletions Sources/NIOFS/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -974,6 +974,17 @@ extension FileSystemError {
)
}

@_spi(Testing)
public static func homeDirectory(errno: Errno, location: SourceLocation) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are structured to follow the underlying syscall so this should be called getpwuid_r

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(
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
61 changes: 61 additions & 0 deletions Sources/NIOFS/Internal/System Calls/Syscall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,67 @@ public enum Libc: Sendable {
}
}

static func homeDirectoryForCurrentUser() -> Result<FilePath, Errno> {
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<FilePath, Errno> {
var pwd = passwd()
var result: UnsafeMutablePointer<passwd>? = 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<String, Errno> {
var buffer = [CInterop.PlatformChar](repeating: 0, count: 128)
Expand Down
11 changes: 11 additions & 0 deletions Sources/NIOFS/Internal/System Calls/Syscalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<passwd>,
_ buffer: UnsafeMutablePointer<CChar>,
_ bufferSize: Int,
_ result: UnsafeMutablePointer<UnsafeMutablePointer<passwd>?>
) -> CInt {
getpwuid_r(uid, pwd, buffer, bufferSize, result)
}

/// confstr(3)
#if !os(Android)
internal func libc_confstr(
Expand Down
11 changes: 11 additions & 0 deletions Sources/_NIOFileSystem/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -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(
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
61 changes: 61 additions & 0 deletions Sources/_NIOFileSystem/Internal/System Calls/Syscall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,67 @@ public enum Libc: Sendable {
}
}

static func homeDirectoryForCurrentUser() -> Result<FilePath, Errno> {
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The #else should have the same guarding as the _homeDirectoryFromPasswordDatabase function does. At the moment it's !os(Windows) which isn't the same as Darwin || Glibc || Musl || Android

return self._homeDirectoryFromPasswordDatabase()
#endif
}

#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Android)
private static func _homeDirectoryFromPasswordDatabase() -> Result<FilePath, Errno> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you refactor this slightly so that it's getpwduid_r and has the uid passed in?

var pwd = passwd()
var result: UnsafeMutablePointer<passwd>? = nil
var buffer = [CChar](repeating: 0, count: 256)

let uid: uid_t = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The syscalls and libc calls in NIOFileSystem are structured so that Syscalls.swift contains top-level functions called system_* and libc_* which call through to the appropriate Darwin/Glibc/Musl etc. so that the caller doesn't need to do that switching.

Then layered on top of that are the functions on the Syscall and Libc enums which call though to system_*/libc_* but use Swift types instead of C types (e.g. String in place of char pointer).

To that end, could you add a system_getuid() function to Syscall.swift and System.getuid() function to Syscall.swift?

#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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than dealing with CInt we have a bunch of helpers that deal in Result types. They also handle calling the underlying function again if it was interrupted, so here I think you need a construct like:

let result = buffer.withUnsafeMutableBufferPointer { pointer in 
    withUnsafeMutablePointer(to &result) { resultPointer in 
        nothingOrErrno(retryOnInterrupt: true) {
            libc_getpwuid_r(uid, &pwd, pointer.baseAddress!, pointer.count, resultPointer)
        }
    }
}

That allows you to switch on the result below and use the Errno type rather than CInt.

guard let baseAddress = pointer.baseAddress else {
return CInt(ERANGE)
}
Comment on lines +400 to +402
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check shouldn't be necessary, buffer won't be empty, it's okay to ! it below.


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<String, Errno> {
var buffer = [CInterop.PlatformChar](repeating: 0, count: 128)
Expand Down
11 changes: 11 additions & 0 deletions Sources/_NIOFileSystem/Internal/System Calls/Syscalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<passwd>,
_ buffer: UnsafeMutablePointer<CChar>,
_ bufferSize: Int,
_ result: UnsafeMutablePointer<UnsafeMutablePointer<passwd>?>
) -> CInt {
getpwuid_r(uid, pwd, buffer, bufferSize, result)
}

/// confstr(3)
#if !os(Android)
internal func libc_confstr(
Expand Down
9 changes: 9 additions & 0 deletions Tests/NIOFSIntegrationTests/FileSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down