mirror of
https://github.com/apple/swift-nio.git
synced 2026-05-20 20:30:36 +00:00
Add homeDirectory accessor to FileSystem ### Motivation: Addresses #3381 by adding a `homeDirectory` property to FileSystem, equivalent to `FileManager.default.homeDirectoryForCurrentUser`. ### Modifications: - Added `homeDirectory` property to `FileSystemProtocol` and implemented in `FileSystem` - Implementation checks `HOME` environment variable first, falls back to `USERPROFILE` on Windows, or uses `getpwuid_r(3)` on POSIX systems - Added system call wrappers (`system_getuid`, `libc_getpwuid_r`) with proper platform guards - Added `FileSystemError.getpwuid_r()` error helper ### Result: Users can now access the home directory via `FileSystem.homeDirectory`, returning a `FilePath` asynchronously. Follows the same pattern as `currentWorkingDirectory` and `temporaryDirectory`. Co-authored-by: Cory Benfield <lukasa@apple.com>
491 lines
15 KiB
Swift
491 lines
15 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import SystemPackage
|
|
|
|
#if canImport(Darwin)
|
|
import Darwin
|
|
import CNIODarwin
|
|
#elseif canImport(Glibc)
|
|
@preconcurrency import Glibc
|
|
import CNIOLinux
|
|
#elseif canImport(Musl)
|
|
@preconcurrency import Musl
|
|
import CNIOLinux
|
|
#elseif canImport(Bionic)
|
|
@preconcurrency import Bionic
|
|
import CNIOLinux
|
|
#endif
|
|
|
|
@_spi(Testing)
|
|
public enum Syscall: Sendable {
|
|
@_spi(Testing)
|
|
public static func stat(path: FilePath) -> Result<CInterop.Stat, Errno> {
|
|
path.withPlatformString { platformPath in
|
|
var status = CInterop.Stat()
|
|
return valueOrErrno(retryOnInterrupt: false) {
|
|
system_stat(platformPath, &status)
|
|
}.map { _ in
|
|
status
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public static func lstat(path: FilePath) -> Result<CInterop.Stat, Errno> {
|
|
path.withPlatformString { platformPath in
|
|
var status = CInterop.Stat()
|
|
return valueOrErrno(retryOnInterrupt: false) {
|
|
system_lstat(platformPath, &status)
|
|
}.map { _ in
|
|
status
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public static func mkdir(at path: FilePath, permissions: FilePermissions) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
path.withPlatformString { p in
|
|
system_mkdir(p, permissions.rawValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public static func rename(from old: FilePath, to new: FilePath) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
old.withPlatformString { oldPath in
|
|
new.withPlatformString { newPath in
|
|
system_rename(oldPath, newPath)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if canImport(Darwin)
|
|
@_spi(Testing)
|
|
public static func rename(
|
|
from old: FilePath,
|
|
to new: FilePath,
|
|
options: RenameOptions
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
old.withPlatformString { oldPath in
|
|
new.withPlatformString { newPath in
|
|
system_renamex_np(oldPath, newPath, options.rawValue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public struct RenameOptions: OptionSet, Sendable {
|
|
public var rawValue: CUnsignedInt
|
|
|
|
public init(rawValue: CUnsignedInt) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
public static var exclusive: Self {
|
|
Self(rawValue: UInt32(bitPattern: RENAME_EXCL))
|
|
}
|
|
|
|
public static var swap: Self {
|
|
Self(rawValue: UInt32(bitPattern: RENAME_SWAP))
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if canImport(Glibc) || canImport(Musl) || canImport(Bionic)
|
|
@_spi(Testing)
|
|
public static func rename(
|
|
from old: FilePath,
|
|
relativeTo oldFD: FileDescriptor,
|
|
to new: FilePath,
|
|
relativeTo newFD: FileDescriptor,
|
|
flags: RenameAtFlags
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
old.withPlatformString { oldPath in
|
|
new.withPlatformString { newPath in
|
|
system_renameat2(
|
|
oldFD.rawValue,
|
|
oldPath,
|
|
newFD.rawValue,
|
|
newPath,
|
|
flags.rawValue
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public struct RenameAtFlags: OptionSet {
|
|
public var rawValue: CUnsignedInt
|
|
|
|
public init(rawValue: CUnsignedInt) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
public static var exclusive: Self {
|
|
Self(rawValue: CNIOLinux_RENAME_NOREPLACE)
|
|
}
|
|
|
|
public static var swap: Self {
|
|
Self(rawValue: CNIOLinux_RENAME_EXCHANGE)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if canImport(Glibc) || canImport(Musl) || canImport(Bionic)
|
|
@_spi(Testing)
|
|
public struct LinkAtFlags: OptionSet {
|
|
@_spi(Testing)
|
|
public var rawValue: CInt
|
|
|
|
@_spi(Testing)
|
|
public init(rawValue: CInt) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public static var emptyPath: Self {
|
|
Self(rawValue: CNIOLinux_AT_EMPTY_PATH)
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public static var followSymbolicLinks: Self {
|
|
Self(rawValue: AT_SYMLINK_FOLLOW)
|
|
}
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public static func linkAt(
|
|
from source: FilePath,
|
|
relativeTo sourceFD: FileDescriptor,
|
|
to destination: FilePath,
|
|
relativeTo destinationFD: FileDescriptor,
|
|
flags: LinkAtFlags
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
source.withPlatformString { src in
|
|
destination.withPlatformString { dst in
|
|
system_linkat(
|
|
sourceFD.rawValue,
|
|
src,
|
|
destinationFD.rawValue,
|
|
dst,
|
|
flags.rawValue
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
@_spi(Testing)
|
|
public static func link(
|
|
from source: FilePath,
|
|
to destination: FilePath
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
source.withPlatformString { src in
|
|
destination.withPlatformString { dst in
|
|
system_link(src, dst)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public static func unlink(path: FilePath) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
path.withPlatformString { ptr in
|
|
system_unlink(ptr)
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public static func symlink(
|
|
to destination: FilePath,
|
|
from source: FilePath
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
source.withPlatformString { src in
|
|
destination.withPlatformString { dst in
|
|
system_symlink(dst, src)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(Testing)
|
|
public static func readlink(at path: FilePath) -> Result<FilePath, Errno> {
|
|
do {
|
|
let resolved = try path.withPlatformString { p in
|
|
try String(customUnsafeUninitializedCapacity: Int(CInterop.maxPathLength)) { pointer in
|
|
let result = pointer.withMemoryRebound(to: CInterop.PlatformChar.self) { ptr in
|
|
valueOrErrno(retryOnInterrupt: false) {
|
|
system_readlink(p, ptr.baseAddress!, ptr.count)
|
|
}
|
|
}
|
|
return try result.get()
|
|
}
|
|
}
|
|
return .success(FilePath(resolved))
|
|
} catch let error as Errno {
|
|
return .failure(error)
|
|
} catch {
|
|
// Shouldn't happen: we deal in Result types and only ever with Errno.
|
|
fatalError("Unexpected error '\(error)' caught")
|
|
}
|
|
}
|
|
|
|
#if canImport(Glibc) || canImport(Musl) || canImport(Bionic)
|
|
@_spi(Testing)
|
|
public static func sendfile(
|
|
to output: FileDescriptor,
|
|
from input: FileDescriptor,
|
|
offset: Int,
|
|
size: Int
|
|
) -> Result<Int, Errno> {
|
|
valueOrErrno(retryOnInterrupt: false) {
|
|
system_sendfile(output.rawValue, input.rawValue, off_t(offset), size)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
@_spi(Testing)
|
|
public static func futimens(
|
|
fileDescriptor fd: FileDescriptor,
|
|
times: UnsafePointer<timespec>?
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
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)
|
|
public enum Libc: Sendable {
|
|
static func readdir(
|
|
_ dir: CInterop.DirPointer
|
|
) -> Result<UnsafeMutablePointer<CInterop.DirEnt>?, Errno> {
|
|
optionalValueOrErrno(retryOnInterrupt: false) {
|
|
libc_readdir(dir)
|
|
}
|
|
}
|
|
|
|
static func closedir(
|
|
_ dir: CInterop.DirPointer
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
libc_closedir(dir)
|
|
}
|
|
}
|
|
|
|
#if canImport(Darwin)
|
|
@_spi(Testing)
|
|
public static func fcopyfile(
|
|
from source: FileDescriptor,
|
|
to destination: FileDescriptor,
|
|
state: copyfile_state_t?,
|
|
flags: copyfile_flags_t
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
libc_fcopyfile(source.rawValue, destination.rawValue, state, flags)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if canImport(Darwin)
|
|
@_spi(Testing)
|
|
public static func copyfile(
|
|
from source: FilePath,
|
|
to destination: FilePath,
|
|
state: copyfile_state_t?,
|
|
flags: copyfile_flags_t
|
|
) -> Result<Void, Errno> {
|
|
source.withPlatformString { sourcePath in
|
|
destination.withPlatformString { destinationPath in
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
libc_copyfile(sourcePath, destinationPath, state, flags)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
@_spi(Testing)
|
|
public static func remove(
|
|
_ path: FilePath
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno(retryOnInterrupt: false) {
|
|
path.withPlatformString {
|
|
libc_remove($0)
|
|
}
|
|
}
|
|
}
|
|
|
|
static func getcwd() -> Result<FilePath, Errno> {
|
|
var buffer = [CInterop.PlatformChar](
|
|
repeating: 0,
|
|
count: Int(CInterop.maxPathLength)
|
|
)
|
|
|
|
return optionalValueOrErrno(retryOnInterrupt: false) {
|
|
buffer.withUnsafeMutableBufferPointer { pointer in
|
|
libc_getcwd(pointer.baseAddress!, pointer.count)
|
|
}
|
|
}.map { ptr in
|
|
// 'ptr' is just the input pointer, we should ignore it and just rely on the bytes
|
|
// in buffer.
|
|
//
|
|
// At this point 'ptr' must be non-nil, because if it were 'nil' we should be on the
|
|
// error path.
|
|
precondition(ptr != nil)
|
|
return FilePath(platformString: buffer)
|
|
}
|
|
}
|
|
|
|
#if !os(Android)
|
|
static func constr(_ name: CInt) -> Result<String, Errno> {
|
|
var buffer = [UInt8](repeating: 0, count: 128)
|
|
|
|
repeat {
|
|
let result = valueOrErrno(retryOnInterrupt: false) {
|
|
buffer.withUnsafeMutableBufferPointer { pointer in
|
|
pointer.withMemoryRebound(to: CInterop.PlatformChar.self) { pointer in
|
|
libc_confstr(name, pointer.baseAddress!, pointer.count)
|
|
}
|
|
}
|
|
}
|
|
|
|
switch result {
|
|
case let .success(length):
|
|
let nullTerminationIndex = buffer.firstIndex(of: 0) ?? buffer.endIndex
|
|
if length <= buffer.count {
|
|
return .success(String(decoding: buffer[..<nullTerminationIndex], as: UTF8.self))
|
|
} else {
|
|
// The buffer wasn't long enough. Double and try again.
|
|
buffer.append(contentsOf: repeatElement(0, count: buffer.capacity))
|
|
}
|
|
case let .failure(errno):
|
|
return .failure(errno)
|
|
}
|
|
} while true
|
|
}
|
|
#endif
|
|
|
|
static func ftsOpen(_ path: FilePath, options: FTSOpenOptions) -> Result<CInterop.FTSPointer, Errno> {
|
|
// 'fts_open' needs an unsafe mutable pointer to the C-string, `FilePath` doesn't offer this
|
|
// so copy out its bytes.
|
|
var pathBytes = path.withPlatformString { pointer in
|
|
// Length excludes the null terminator, so add it back.
|
|
let bufferPointer = UnsafeBufferPointer(start: pointer, count: path.length + 1)
|
|
return Array(bufferPointer)
|
|
}
|
|
|
|
return valueOrErrno {
|
|
pathBytes.withUnsafeMutableBufferPointer { pointer in
|
|
// The array must be terminated with a nil.
|
|
libc_fts_open([pointer.baseAddress, nil], options.rawValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Options passed to 'fts_open'.
|
|
struct FTSOpenOptions: OptionSet {
|
|
var rawValue: CInt
|
|
|
|
/// Don't change directory while walking the filesystem hierarchy.
|
|
static var noChangeDir: Self { Self(rawValue: FTS_NOCHDIR) }
|
|
|
|
/// Return FTS entries for symbolic links rather than their targets.
|
|
static var physical: Self { Self(rawValue: FTS_PHYSICAL) }
|
|
|
|
/// Return FTS entries for the targets of symbolic links.
|
|
static var logical: Self { Self(rawValue: FTS_LOGICAL) }
|
|
}
|
|
|
|
static func ftsRead(
|
|
_ pointer: CInterop.FTSPointer
|
|
) -> Result<UnsafeMutablePointer<CInterop.FTSEnt>?, Errno> {
|
|
optionalValueOrErrno {
|
|
libc_fts_read(pointer)
|
|
}
|
|
}
|
|
|
|
static func ftsClose(
|
|
_ pointer: CInterop.FTSPointer
|
|
) -> Result<Void, Errno> {
|
|
nothingOrErrno {
|
|
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
|
|
}
|