diff --git a/Sources/System/FileSystem/FileFlags.swift b/Sources/System/FileSystem/FileFlags.swift new file mode 100644 index 00000000..fd714cb8 --- /dev/null +++ b/Sources/System/FileSystem/FileFlags.swift @@ -0,0 +1,254 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// |------------------------| +// | Swift API to C Mapping | +// |------------------------------------------------------------------| +// | FileFlags | Darwin | FreeBSD | OpenBSD | +// |------------------|---------------|---------------|---------------| +// | noDump | UF_NODUMP | UF_NODUMP | UF_NODUMP | +// | userImmutable | UF_IMMUTABLE | UF_IMMUTABLE | UF_IMMUTABLE | +// | userAppend | UF_APPEND | UF_APPEND | UF_APPEND | +// | archived | SF_ARCHIVED | SF_ARCHIVED | SF_ARCHIVED | +// | systemImmutable | SF_IMMUTABLE | SF_IMMUTABLE | SF_IMMUTABLE | +// | systemAppend | SF_APPEND | SF_APPEND | SF_APPEND | +// | opaque | UF_OPAQUE | UF_OPAQUE | N/A | +// | hidden | UF_HIDDEN | UF_HIDDEN | N/A | +// | systemNoUnlink | SF_NOUNLINK | SF_NOUNLINK | N/A | +// | compressed | UF_COMPRESSED | N/A | N/A | +// | tracked | UF_TRACKED | N/A | N/A | +// | dataVault | UF_DATAVAULT | N/A | N/A | +// | restricted | SF_RESTRICTED | N/A | N/A | +// | firmlink | SF_FIRMLINK | N/A | N/A | +// | dataless | SF_DATALESS | N/A | N/A | +// | userNoUnlink | N/A | UF_NOUNLINK | N/A | +// | offline | N/A | UF_OFFLINE | N/A | +// | readOnly | N/A | UF_READONLY | N/A | +// | reparse | N/A | UF_REPARSE | N/A | +// | sparse | N/A | UF_SPARSE | N/A | +// | system | N/A | UF_SYSTEM | N/A | +// | snapshot | N/A | SF_SNAPSHOT | N/A | +// |------------------|---------------|---------------|---------------| + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) +// @available(System X.Y.Z, *) +extension CInterop { + public typealias FileFlags = UInt32 +} + +/// File-specific flags found in the `st_flags` property of a `stat` struct +/// or used as input to `chflags()`. +/// +/// - Note: Only available on Darwin, FreeBSD, and OpenBSD. +@frozen +// @available(System X.Y.Z, *) +public struct FileFlags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + @_alwaysEmitIntoClient + public let rawValue: CInterop.FileFlags + + /// Creates a strongly-typed `FileFlags` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FileFlags) { self.rawValue = rawValue } + + // MARK: Flags Available on Darwin, FreeBSD, and OpenBSD + + /// Do not dump the file during backups. + /// + /// The corresponding C constant is `UF_NODUMP`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var noDump: FileFlags { FileFlags(rawValue: _UF_NODUMP) } + + /// File may not be changed. + /// + /// The corresponding C constant is `UF_IMMUTABLE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var userImmutable: FileFlags { FileFlags(rawValue: _UF_IMMUTABLE) } + + /// Writes to the file may only append. + /// + /// The corresponding C constant is `UF_APPEND`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var userAppend: FileFlags { FileFlags(rawValue: _UF_APPEND) } + + /// File has been archived. + /// + /// The corresponding C constant is `SF_ARCHIVED`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var archived: FileFlags { FileFlags(rawValue: _SF_ARCHIVED) } + + /// File may not be changed. + /// + /// The corresponding C constant is `SF_IMMUTABLE`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var systemImmutable: FileFlags { FileFlags(rawValue: _SF_IMMUTABLE) } + + /// Writes to the file may only append. + /// + /// The corresponding C constant is `SF_APPEND`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var systemAppend: FileFlags { FileFlags(rawValue: _SF_APPEND) } + + // MARK: Flags Available on Darwin and FreeBSD + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Directory is opaque when viewed through a union mount. + /// + /// The corresponding C constant is `UF_OPAQUE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var opaque: FileFlags { FileFlags(rawValue: _UF_OPAQUE) } + + /// File should not be displayed in a GUI. + /// + /// The corresponding C constant is `UF_HIDDEN`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var hidden: FileFlags { FileFlags(rawValue: _UF_HIDDEN) } + + /// File may not be removed or renamed. + /// + /// The corresponding C constant is `SF_NOUNLINK`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var systemNoUnlink: FileFlags { FileFlags(rawValue: _SF_NOUNLINK) } + #endif + + // MARK: Flags Available on Darwin only + + #if SYSTEM_PACKAGE_DARWIN + /// File is compressed at the file system level. + /// + /// The corresponding C constant is `UF_COMPRESSED`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + @_alwaysEmitIntoClient + public static var compressed: FileFlags { FileFlags(rawValue: _UF_COMPRESSED) } + + /// File is tracked for the purpose of document IDs. + /// + /// The corresponding C constant is `UF_TRACKED`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var tracked: FileFlags { FileFlags(rawValue: _UF_TRACKED) } + + /// File requires an entitlement for reading and writing. + /// + /// The corresponding C constant is `UF_DATAVAULT`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var dataVault: FileFlags { FileFlags(rawValue: _UF_DATAVAULT) } + + /// File requires an entitlement for writing. + /// + /// The corresponding C constant is `SF_RESTRICTED`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var restricted: FileFlags { FileFlags(rawValue: _SF_RESTRICTED) } + + /// File is a firmlink. + /// + /// Firmlinks are used by macOS to create transparent links between + /// the read-only system volume and writable data volume. For example, + /// the `/Applications` folder on the system volume is a firmlink to + /// the `/Applications` folder on the data volume, allowing the user + /// to see both system- and user-installed applications in a single folder. + /// + /// The corresponding C constant is `SF_FIRMLINK`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var firmlink: FileFlags { FileFlags(rawValue: _SF_FIRMLINK) } + + /// File is a dataless placeholder (content is stored remotely). + /// + /// The system will attempt to materialize the file when accessed according to + /// the dataless file materialization policy of the accessing thread or process. + /// See `getiopolicy_np(3)`. + /// + /// The corresponding C constant is `SF_DATALESS`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + @_alwaysEmitIntoClient + public static var dataless: FileFlags { FileFlags(rawValue: _SF_DATALESS) } + #endif + + // MARK: Flags Available on FreeBSD Only + + #if os(FreeBSD) + /// File may not be removed or renamed. + /// + /// The corresponding C constant is `UF_NOUNLINK`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var userNoUnlink: FileFlags { FileFlags(rawValue: _UF_NOUNLINK) } + + /// File has the Windows offline attribute. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_OFFLINE` attribute, + /// but otherwise provide no special handling when it's set. + /// + /// The corresponding C constant is `UF_OFFLINE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var offline: FileFlags { FileFlags(rawValue: _UF_OFFLINE) } + + /// File is read-only. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_READONLY` attribute. + /// + /// The corresponding C constant is `UF_READONLY`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var readOnly: FileFlags { FileFlags(rawValue: _UF_READONLY) } + + /// File contains a Windows reparse point. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_REPARSE_POINT` attribute. + /// + /// The corresponding C constant is `UF_REPARSE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var reparse: FileFlags { FileFlags(rawValue: _UF_REPARSE) } + + /// File is sparse. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_SPARSE_FILE` attribute, + /// or to indicate a sparse file. + /// + /// The corresponding C constant is `UF_SPARSE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var sparse: FileFlags { FileFlags(rawValue: _UF_SPARSE) } + + /// File has the Windows system attribute. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_SYSTEM` attribute, + /// but otherwise provide no special handling when it's set. + /// + /// The corresponding C constant is `UF_SYSTEM`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var system: FileFlags { FileFlags(rawValue: _UF_SYSTEM) } + + /// File is a snapshot. + /// + /// The corresponding C constant is `SF_SNAPSHOT`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var snapshot: FileFlags { FileFlags(rawValue: _SF_SNAPSHOT) } + #endif +} +#endif diff --git a/Sources/System/FileSystem/FileMode.swift b/Sources/System/FileSystem/FileMode.swift new file mode 100644 index 00000000..9a099476 --- /dev/null +++ b/Sources/System/FileSystem/FileMode.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) +/// A strongly-typed file mode representing a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +// @available(System X.Y.Z, *) +public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C mode. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed `FileMode` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Mode) { self.rawValue = rawValue } + + /// Creates a `FileMode` from the given file type and permissions. + /// + /// - Note: This initializer masks the inputs with their respective bit masks. + @_alwaysEmitIntoClient + public init(type: FileType, permissions: FilePermissions) { + self.rawValue = (type.rawValue & _MODE_FILETYPE_MASK) | (permissions.rawValue & _MODE_PERMISSIONS_MASK) + } + + /// The file's type, from the mode's file-type bits. + /// + /// Setting this property will mask the `newValue` with the file-type bit mask `S_IFMT`. + @_alwaysEmitIntoClient + public var type: FileType { + get { FileType(rawValue: rawValue & _MODE_FILETYPE_MASK) } + set { rawValue = (rawValue & ~_MODE_FILETYPE_MASK) | (newValue.rawValue & _MODE_FILETYPE_MASK) } + } + + /// The file's permissions, from the mode's permission bits. + /// + /// Setting this property will mask the `newValue` with the permissions bit mask `ALLPERMS`. + @_alwaysEmitIntoClient + public var permissions: FilePermissions { + get { FilePermissions(rawValue: rawValue & _MODE_PERMISSIONS_MASK) } + set { rawValue = (rawValue & ~_MODE_PERMISSIONS_MASK) | (newValue.rawValue & _MODE_PERMISSIONS_MASK) } + } +} +#endif diff --git a/Sources/System/FileSystem/FileType.swift b/Sources/System/FileSystem/FileType.swift new file mode 100644 index 00000000..638b1472 --- /dev/null +++ b/Sources/System/FileSystem/FileType.swift @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// |------------------------| +// | Swift API to C Mapping | +// |----------------------------------------| +// | FileType | Unix-like Platforms | +// |------------------|---------------------| +// | directory | S_IFDIR | +// | characterSpecial | S_IFCHR | +// | blockSpecial | S_IFBLK | +// | regular | S_IFREG | +// | fifo | S_IFIFO | +// | symbolicLink | S_IFLNK | +// | socket | S_IFSOCK | +// |------------------|---------------------| +// +// |------------------------------------------------------------------| +// | FileType | Darwin | FreeBSD | Other Unix-like Platforms | +// |------------------|---------|---------|---------------------------| +// | whiteout | S_IFWHT | S_IFWHT | N/A | +// |------------------|---------|---------|---------------------------| + +#if !os(Windows) +/// A file type matching those contained in a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +// @available(System X.Y.Z, *) +public struct FileType: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw file-type bits from the C mode. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed file type from the raw C `mode_t`. + /// + /// - Note: This initializer stores the `rawValue` directly and **does not** + /// mask the value with `S_IFMT`. If the supplied `rawValue` contains bits + /// outside of the `S_IFMT` mask, the resulting `FileType` will not compare + /// equal to constants like `.directory` and `.symbolicLink`, which may + /// be unexpected. + /// + /// If you're unsure whether the `mode_t` contains bits outside of `S_IFMT`, + /// you can use `FileMode(rawValue:)` instead to get a strongly-typed + /// `FileMode`, then call `.type` to get the properly masked `FileType`. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Mode) { self.rawValue = rawValue } + + /// Directory + /// + /// The corresponding C constant is `S_IFDIR`. + @_alwaysEmitIntoClient + public static var directory: FileType { FileType(rawValue: _S_IFDIR) } + + /// Character special device + /// + /// The corresponding C constant is `S_IFCHR`. + @_alwaysEmitIntoClient + public static var characterSpecial: FileType { FileType(rawValue: _S_IFCHR) } + + /// Block special device + /// + /// The corresponding C constant is `S_IFBLK`. + @_alwaysEmitIntoClient + public static var blockSpecial: FileType { FileType(rawValue: _S_IFBLK) } + + /// Regular file + /// + /// The corresponding C constant is `S_IFREG`. + @_alwaysEmitIntoClient + public static var regular: FileType { FileType(rawValue: _S_IFREG) } + + /// FIFO (or named pipe) + /// + /// The corresponding C constant is `S_IFIFO`. + @_alwaysEmitIntoClient + public static var fifo: FileType { FileType(rawValue: _S_IFIFO) } + + /// Symbolic link + /// + /// The corresponding C constant is `S_IFLNK`. + @_alwaysEmitIntoClient + public static var symbolicLink: FileType { FileType(rawValue: _S_IFLNK) } + + /// Socket + /// + /// The corresponding C constant is `S_IFSOCK`. + @_alwaysEmitIntoClient + public static var socket: FileType { FileType(rawValue: _S_IFSOCK) } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Whiteout file + /// + /// The corresponding C constant is `S_IFWHT`. + @_alwaysEmitIntoClient + public static var whiteout: FileType { FileType(rawValue: _S_IFWHT) } + #endif +} +#endif diff --git a/Sources/System/FileSystem/Identifiers.swift b/Sources/System/FileSystem/Identifiers.swift new file mode 100644 index 00000000..7620b601 --- /dev/null +++ b/Sources/System/FileSystem/Identifiers.swift @@ -0,0 +1,106 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) +/// A Swift wrapper of the C `uid_t` type. +@frozen +// @available(System X.Y.Z, *) +public struct UserID: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `uid_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.UserID + + /// Creates a strongly-typed `UserID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.UserID) { self.rawValue = rawValue } + + /// Creates a strongly-typed `UserID` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.UserID) { self.rawValue = rawValue } +} + +/// A Swift wrapper of the C `gid_t` type. +@frozen +// @available(System X.Y.Z, *) +public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `gid_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.GroupID + + /// Creates a strongly-typed `GroupID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.GroupID) { self.rawValue = rawValue } + + /// Creates a strongly-typed `GroupID` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.GroupID) { self.rawValue = rawValue } +} + +/// A Swift wrapper of the C `dev_t` type. +@frozen +// @available(System X.Y.Z, *) +public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `dev_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.DeviceID + + /// Creates a strongly-typed `DeviceID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.DeviceID) { self.rawValue = rawValue } + + /// Creates a strongly-typed `DeviceID` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.DeviceID) { self.rawValue = rawValue } + + // TODO: API review for ID wrapper functionality + +// /// Creates a `DeviceID` from the given major and minor device numbers. +// /// +// /// The corresponding C function is `makedev()`. +// @_alwaysEmitIntoClient +// private static func make(major: CUnsignedInt, minor: CUnsignedInt) -> DeviceID { +// DeviceID(rawValue: system_makedev(major, minor)) +// } +// +// /// The major device number +// /// +// /// The corresponding C function is `major()`. +// @_alwaysEmitIntoClient +// private var major: CInt { system_major(rawValue) } +// +// /// The minor device number +// /// +// /// The corresponding C function is `minor()`. +// @_alwaysEmitIntoClient +// private var minor: CInt { system_minor(rawValue) } +} + +/// A Swift wrapper of the C `ino_t` type. +@frozen +// @available(System X.Y.Z, *) +public struct Inode: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `ino_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Inode + + /// Creates a strongly-typed `Inode` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Inode) { self.rawValue = rawValue } + + /// Creates a strongly-typed `Inode` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.Inode) { self.rawValue = rawValue } +} +#endif // !os(Windows) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift new file mode 100644 index 00000000..31243e68 --- /dev/null +++ b/Sources/System/FileSystem/Stat.swift @@ -0,0 +1,749 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) + +// Must import here to use C stat properties in @_alwaysEmitIntoClient APIs. +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import CSystem +import Glibc +#elseif canImport(Musl) +import CSystem +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Android) +import CSystem +import Android +#else +#error("Unsupported Platform") +#endif + +// MARK: - Stat + +/// A Swift wrapper of the C `stat` struct. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +// @available(System X.Y.Z, *) +public struct Stat: RawRepresentable, Sendable { + + /// The raw C `stat` struct. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Stat + + /// Creates a Swift `Stat` from the raw C struct. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Stat) { self.rawValue = rawValue } + + // MARK: Stat.Flags + + /// Flags representing those passed to `fstatat()`. + @frozen + public struct Flags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly-typed `Stat.Flags` from raw C flags. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + /// If the path ends with a symbolic link, return information about the link itself. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW`. + @_alwaysEmitIntoClient + public static var symlinkNoFollow: Flags { Flags(rawValue: _AT_SYMLINK_NOFOLLOW) } + + #if SYSTEM_PACKAGE_DARWIN + /// If the path ends with a symbolic link, return information about the link itself. + /// If _any_ symbolic link is encountered during path resolution, return an error. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW_ANY`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var symlinkNoFollowAny: Flags { Flags(rawValue: _AT_SYMLINK_NOFOLLOW_ANY) } + #endif + + #if canImport(Darwin, _version: 346) || os(FreeBSD) + /// If the path does not reside in the hierarchy beneath the starting directory, return an error. + /// + /// The corresponding C constant is `AT_RESOLVE_BENEATH`. + /// - Note: Only available on Darwin and FreeBSD. + @_alwaysEmitIntoClient + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) + public static var resolveBeneath: Flags { Flags(rawValue: _AT_RESOLVE_BENEATH) } + #endif + + // TODO: Re-enable when _GNU_SOURCE can be defined. +// #if os(FreeBSD) || os(Linux) || os(Android) +// /// If the path is an empty string (or `NULL` since Linux 6.11), +// /// return information about the given file descriptor. +// /// +// /// The corresponding C constant is `AT_EMPTY_PATH`. +// /// - Note: Only available on FreeBSD, Linux, and Android. +// @_alwaysEmitIntoClient +// public static var emptyPath: Flags { Flags(rawValue: _AT_EMPTY_PATH) } +// #endif + } + + // MARK: Initializers + + /// Creates a `Stat` struct from a `FilePath`. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._stat( + $0, + followTargetSymlink: followTargetSymlink, + retryOnInterrupt: retryOnInterrupt + ) + }.get() + } + + /// Creates a `Stat` struct from an `UnsafePointer` path. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._stat( + path, + followTargetSymlink: followTargetSymlink, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _stat( + _ ptr: UnsafePointer, + followTargetSymlink: Bool, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + if followTargetSymlink { + system_stat(ptr, &result) + } else { + system_lstat(ptr, &result) + } + }.map { result } + } + + /// Creates a `Stat` struct from a `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + @_alwaysEmitIntoClient + public init( + _ fd: FileDescriptor, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstat( + fd, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _fstat( + _ fd: FileDescriptor, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstat(fd.rawValue, &result) + }.map { result } + } + + /// Creates a `Stat` struct from a `FilePath` and `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._fstatat( + $0, + relativeTo: _AT_FDCWD, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ) + }.get() + } + + /// Creates a `Stat` struct from a `FilePath` and `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._fstatat( + $0, + relativeTo: fd.rawValue, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ) + }.get() + } + + /// Creates a `Stat` struct from an `UnsafePointer` path and `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstatat( + path, + relativeTo: _AT_FDCWD, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + /// Creates a `Stat` struct from an `UnsafePointer` path and `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstatat( + path, + relativeTo: fd.rawValue, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _fstatat( + _ path: UnsafePointer, + relativeTo fd: FileDescriptor.RawValue, + flags: Stat.Flags, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstatat(fd, path, &result, flags.rawValue) + }.map { result } + } + + + // MARK: Properties + + /// ID of device containing file + /// + /// The corresponding C property is `st_dev`. + @_alwaysEmitIntoClient + public var deviceID: DeviceID { + get { DeviceID(rawValue: rawValue.st_dev) } + set { rawValue.st_dev = newValue.rawValue } + } + + /// Inode number + /// + /// The corresponding C property is `st_ino`. + @_alwaysEmitIntoClient + public var inode: Inode { + get { Inode(rawValue: rawValue.st_ino) } + set { rawValue.st_ino = newValue.rawValue } + } + + /// File mode + /// + /// The corresponding C property is `st_mode`. + @_alwaysEmitIntoClient + public var mode: FileMode { + get { FileMode(rawValue: rawValue.st_mode) } + set { rawValue.st_mode = newValue.rawValue } + } + + /// File type for the given mode + /// + /// - Note: This property is equivalent to `mode.type`. Modifying this + /// property will update the underlying `st_mode` accordingly. + @_alwaysEmitIntoClient + public var type: FileType { + get { mode.type } + set { + var newMode = mode + newMode.type = newValue + mode = newMode + } + } + + /// File permissions for the given mode + /// + /// - Note: This property is equivalent to `mode.permissions`. Modifying + /// this property will update the underlying `st_mode` accordingly. + @_alwaysEmitIntoClient + public var permissions: FilePermissions { + get { mode.permissions } + set { + var newMode = mode + newMode.permissions = newValue + mode = newMode + } + } + + /// Number of hard links + /// + /// The corresponding C property is `st_nlink`. + @_alwaysEmitIntoClient + public var linkCount: Int { + get { Int(rawValue.st_nlink) } + set { rawValue.st_nlink = numericCast(newValue) } + } + + /// User ID of owner + /// + /// The corresponding C property is `st_uid`. + @_alwaysEmitIntoClient + public var userID: UserID { + get { UserID(rawValue: rawValue.st_uid) } + set { rawValue.st_uid = newValue.rawValue } + } + + /// Group ID of owner + /// + /// The corresponding C property is `st_gid`. + @_alwaysEmitIntoClient + public var groupID: GroupID { + get { GroupID(rawValue: rawValue.st_gid) } + set { rawValue.st_gid = newValue.rawValue } + } + + /// Device ID (if special file) + /// + /// For character or block special files, the returned `DeviceID` may have + /// meaningful major and minor values. For non-special files, this + /// property is usually meaningless and often set to 0. + /// + /// The corresponding C property is `st_rdev`. + @_alwaysEmitIntoClient + public var specialDeviceID: DeviceID { + get { DeviceID(rawValue: rawValue.st_rdev) } + set { rawValue.st_rdev = newValue.rawValue } + } + + /// Total size, in bytes + /// + /// The semantics of this property are tied to the underlying C `st_size` field, + /// which can have file system-dependent behavior. For example, this property + /// can return different values for a file's data fork and resource fork, and some + /// file systems report logical size rather than actual disk usage for compressed + /// or cloned files. + /// + /// The corresponding C property is `st_size`. + @_alwaysEmitIntoClient + public var size: Int64 { + get { Int64(rawValue.st_size) } + set { rawValue.st_size = numericCast(newValue) } + } + + /// Block size for filesystem I/O, in bytes + /// + /// The corresponding C property is `st_blksize`. + @_alwaysEmitIntoClient + public var preferredIOBlockSize: Int { + get { Int(rawValue.st_blksize) } + set { rawValue.st_blksize = numericCast(newValue) } + } + + /// Number of 512-byte blocks allocated + /// + /// The semantics of this property are tied to the underlying C `st_blocks` field, + /// which can have file system-dependent behavior. + /// + /// The corresponding C property is `st_blocks`. + @_alwaysEmitIntoClient + public var blocksAllocated: Int64 { + get { Int64(rawValue.st_blocks) } + set { rawValue.st_blocks = numericCast(newValue) } + } + + /// Total size allocated, in bytes + /// + /// The semantics of this property are tied to the underlying C `st_blocks` field, + /// which can have file system-dependent behavior. + /// + /// - Note: Calculated as `512 * blocksAllocated`. + @_alwaysEmitIntoClient + public var sizeAllocated: Int64 { + 512 * blocksAllocated + } + + // NOTE: "st_" property names are used for the `timespec` properties so + // we can reserve `accessTime`, `modificationTime`, etc. for potential + // `UTCClock.Instant` properties in the future. + + /// Time of last access, given as a C `timespec` since the Epoch. + /// + /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). + @_alwaysEmitIntoClient + public var st_atim: timespec { + get { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_atimespec + #else + rawValue.st_atim + #endif + } + set { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_atimespec = newValue + #else + rawValue.st_atim = newValue + #endif + } + } + + /// Time of last modification, given as a C `timespec` since the Epoch. + /// + /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). + @_alwaysEmitIntoClient + public var st_mtim: timespec { + get { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_mtimespec + #else + rawValue.st_mtim + #endif + } + set { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_mtimespec = newValue + #else + rawValue.st_mtim = newValue + #endif + } + } + + /// Time of last status (inode) change, given as a C `timespec` since the Epoch. + /// + /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). + @_alwaysEmitIntoClient + public var st_ctim: timespec { + get { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_ctimespec + #else + rawValue.st_ctim + #endif + } + set { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_ctimespec = newValue + #else + rawValue.st_ctim = newValue + #endif + } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Time of file creation, given as a C `timespec` since the Epoch. + /// + /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). + /// - Note: Only available on Darwin and FreeBSD. + @_alwaysEmitIntoClient + public var st_birthtim: timespec { + get { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_birthtimespec + #else + rawValue.st_birthtim + #endif + } + set { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_birthtimespec = newValue + #else + rawValue.st_birthtim = newValue + #endif + } + } + #endif + + // TODO: Investigate changing time properties to UTCClock.Instant once available. + +// /// Time of last access, given as a `UTCClock.Instant` +// /// +// /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). +// public var accessTime: UTCClock.Instant { +// get { +// UTCClock.systemEpoch.advanced(by: Duration(st_atim)) +// } +// set { +// st_atim = timespec(UTCClock.systemEpoch.duration(to: newValue)) +// } +// } +// +// /// Time of last modification, given as a `UTCClock.Instant` +// /// +// /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). +// public var modificationTime: UTCClock.Instant { +// get { +// UTCClock.systemEpoch.advanced(by: Duration(st_mtim)) +// } +// set { +// st_mtim = timespec(UTCClock.systemEpoch.duration(to: newValue)) +// } +// } +// +// /// Time of last status (inode) change, given as a `UTCClock.Instant` +// /// +// /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). +// public var changeTime: UTCClock.Instant { +// get { +// UTCClock.systemEpoch.advanced(by: Duration(st_ctim)) +// } +// set { +// st_ctim = timespec(UTCClock.systemEpoch.duration(to: newValue)) +// } +// } +// +// #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +// /// Time of file creation, given as a `UTCClock.Instant` +// /// +// /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). +// /// - Note: Only available on Darwin and FreeBSD. +// public var creationTime: UTCClock.Instant { +// get { +// UTCClock.systemEpoch.advanced(by: Duration(st_birthtim)) +// } +// set { +// st_birthtim = timespec(UTCClock.systemEpoch.duration(to: newValue)) +// } +// } +// #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + /// File flags + /// + /// The corresponding C property is `st_flags`. + /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. + @_alwaysEmitIntoClient + public var flags: FileFlags { + get { FileFlags(rawValue: rawValue.st_flags) } + set { rawValue.st_flags = newValue.rawValue } + } + + /// File generation number + /// + /// The file generation number is used to distinguish between different files + /// that have used the same inode over time. + /// + /// The corresponding C property is `st_gen`. + /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. + @_alwaysEmitIntoClient + public var generationNumber: Int { + get { Int(rawValue.st_gen) } + set { rawValue.st_gen = numericCast(newValue)} + } + #endif +} + +// MARK: - Equatable and Hashable + +extension Stat: Equatable { + @_alwaysEmitIntoClient + /// Compares the raw bytes of two `Stat` structs for equality. + public static func == (lhs: Self, rhs: Self) -> Bool { + return withUnsafeBytes(of: lhs.rawValue) { lhsBytes in + withUnsafeBytes(of: rhs.rawValue) { rhsBytes in + lhsBytes.elementsEqual(rhsBytes) + } + } + } +} + +extension Stat: Hashable { + @_alwaysEmitIntoClient + /// Hashes the raw bytes of this `Stat` struct. + public func hash(into hasher: inout Hasher) { + withUnsafeBytes(of: rawValue) { bytes in + hasher.combine(bytes: bytes) + } + } +} + +// MARK: - CustomStringConvertible and CustomDebugStringConvertible + +// MARK: - FileDescriptor Extensions + +// @available(System X.Y.Z, *) +extension FileDescriptor { + + /// Creates a `Stat` struct for the file referenced by this `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + @_alwaysEmitIntoClient + public func stat( + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try _fstat( + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal func _fstat( + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstat(self.rawValue, &result) + }.map { Stat(rawValue: result) } + } +} + +// MARK: - FilePath Extensions + +// @available(System X.Y.Z, *) +extension FilePath { + + /// Creates a `Stat` struct for the file referenced by this `FilePath`. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + @_alwaysEmitIntoClient + public func stat( + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try _stat( + followTargetSymlink: followTargetSymlink, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal func _stat( + followTargetSymlink: Bool, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return withPlatformString { ptr in + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + if followTargetSymlink { + system_stat(ptr, &result) + } else { + system_lstat(ptr, &result) + } + }.map { Stat(rawValue: result) } + } + } + + /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public func stat( + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try _fstatat( + relativeTo: _AT_FDCWD, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public func stat( + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try _fstatat( + relativeTo: fd.rawValue, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal func _fstatat( + relativeTo fd: FileDescriptor.RawValue, + flags: Stat.Flags, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return withPlatformString { ptr in + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstatat(fd, ptr, &result, flags.rawValue) + }.map { Stat(rawValue: result) } + } + } +} + +#endif // !os(Windows) diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index b6de1233..7a35b09c 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -5,7 +5,7 @@ Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information -*/ + */ #if SYSTEM_PACKAGE_DARWIN import Darwin @@ -78,4 +78,12 @@ public enum CInterop { /// on API. public typealias PlatformUnicodeEncoding = UTF8 #endif + + #if !os(Windows) + public typealias Stat = stat + public typealias DeviceID = dev_t + public typealias Inode = ino_t + public typealias UserID = uid_t + public typealias GroupID = gid_t + #endif } diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index d8cbdcbd..3d4b7efd 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -5,7 +5,7 @@ Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information -*/ + */ // For platform constants redefined in Swift. We define them here so that // they can be used anywhere without imports and without confusion to @@ -17,6 +17,7 @@ import Darwin import CSystem import ucrt #elseif canImport(Glibc) +import CSystem import Glibc #elseif canImport(Musl) import CSystem @@ -438,7 +439,7 @@ internal var _ENOSR: CInt { ENOSR } @_alwaysEmitIntoClient internal var _ENOSTR: CInt { ENOSTR } -#endif +#endif #endif @_alwaysEmitIntoClient @@ -639,3 +640,151 @@ internal var _SEEK_HOLE: CInt { SEEK_HOLE } @_alwaysEmitIntoClient internal var _SEEK_DATA: CInt { SEEK_DATA } #endif + +// MARK: - File System + +#if !os(Windows) + +@_alwaysEmitIntoClient +internal var _AT_FDCWD: CInt { AT_FDCWD } + +// MARK: - fstatat Flags + +@_alwaysEmitIntoClient +internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_FOLLOW } + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } +#endif + +#if canImport(Darwin, _version: 346) || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } +#endif + +// TODO: Re-enable when _GNU_SOURCE can be defined. +//#if os(FreeBSD) || os(Linux) || os(Android) +//@_alwaysEmitIntoClient +//internal var _AT_EMPTY_PATH: CInt { AT_EMPTY_PATH } +//#endif + +// MARK: - File Mode / File Type + +@_alwaysEmitIntoClient +internal var _MODE_FILETYPE_MASK: CInterop.Mode { S_IFMT } + +@_alwaysEmitIntoClient +internal var _MODE_PERMISSIONS_MASK: CInterop.Mode { 0o7777 } + +@_alwaysEmitIntoClient +internal var _S_IFDIR: CInterop.Mode { S_IFDIR } + +@_alwaysEmitIntoClient +internal var _S_IFCHR: CInterop.Mode { S_IFCHR } + +@_alwaysEmitIntoClient +internal var _S_IFBLK: CInterop.Mode { S_IFBLK } + +@_alwaysEmitIntoClient +internal var _S_IFREG: CInterop.Mode { S_IFREG } + +@_alwaysEmitIntoClient +internal var _S_IFIFO: CInterop.Mode { S_IFIFO } + +@_alwaysEmitIntoClient +internal var _S_IFLNK: CInterop.Mode { S_IFLNK } + +@_alwaysEmitIntoClient +internal var _S_IFSOCK: CInterop.Mode { S_IFSOCK } + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _S_IFWHT: CInterop.Mode { S_IFWHT } +#endif + +// MARK: - stat/chflags File Flags + +// MARK: Flags Available on Darwin, FreeBSD, and OpenBSD + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) +@_alwaysEmitIntoClient +internal var _UF_NODUMP: CInterop.FileFlags { UInt32(bitPattern: UF_NODUMP) } + +@_alwaysEmitIntoClient +internal var _UF_IMMUTABLE: CInterop.FileFlags { UInt32(bitPattern: UF_IMMUTABLE) } + +@_alwaysEmitIntoClient +internal var _UF_APPEND: CInterop.FileFlags { UInt32(bitPattern: UF_APPEND) } + +@_alwaysEmitIntoClient +internal var _SF_ARCHIVED: CInterop.FileFlags { UInt32(bitPattern: SF_ARCHIVED) } + +@_alwaysEmitIntoClient +internal var _SF_IMMUTABLE: CInterop.FileFlags { UInt32(bitPattern: SF_IMMUTABLE) } + +@_alwaysEmitIntoClient +internal var _SF_APPEND: CInterop.FileFlags { UInt32(bitPattern: SF_APPEND) } +#endif + +// MARK: Flags Available on Darwin and FreeBSD + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _UF_OPAQUE: CInterop.FileFlags { UInt32(bitPattern: UF_OPAQUE) } + +@_alwaysEmitIntoClient +internal var _UF_HIDDEN: CInterop.FileFlags { UInt32(bitPattern: UF_HIDDEN) } + +@_alwaysEmitIntoClient +internal var _SF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: SF_NOUNLINK) } +#endif + +// MARK: Flags Available on Darwin Only + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _UF_COMPRESSED: CInterop.FileFlags { UInt32(bitPattern: UF_COMPRESSED) } + +@_alwaysEmitIntoClient +internal var _UF_TRACKED: CInterop.FileFlags { UInt32(bitPattern: UF_TRACKED) } + +@_alwaysEmitIntoClient +internal var _UF_DATAVAULT: CInterop.FileFlags { UInt32(bitPattern: UF_DATAVAULT) } + +@_alwaysEmitIntoClient +internal var _SF_RESTRICTED: CInterop.FileFlags { UInt32(bitPattern: SF_RESTRICTED) } + +@_alwaysEmitIntoClient +internal var _SF_FIRMLINK: CInterop.FileFlags { UInt32(bitPattern: SF_FIRMLINK) } + +@_alwaysEmitIntoClient +internal var _SF_DATALESS: CInterop.FileFlags { UInt32(bitPattern: SF_DATALESS) } +#endif + +// MARK: Flags Available on FreeBSD Only + +#if os(FreeBSD) +@_alwaysEmitIntoClient +internal var _UF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: UF_NOUNLINK) } + +@_alwaysEmitIntoClient +internal var _UF_OFFLINE: CInterop.FileFlags { UInt32(bitPattern: UF_OFFLINE) } + +@_alwaysEmitIntoClient +internal var _UF_READONLY: CInterop.FileFlags { UInt32(bitPattern: UF_READONLY) } + +@_alwaysEmitIntoClient +internal var _UF_REPARSE: CInterop.FileFlags { UInt32(bitPattern: UF_REPARSE) } + +@_alwaysEmitIntoClient +internal var _UF_SPARSE: CInterop.FileFlags { UInt32(bitPattern: UF_SPARSE) } + +@_alwaysEmitIntoClient +internal var _UF_SYSTEM: CInterop.FileFlags { UInt32(bitPattern: UF_SYSTEM) } + +@_alwaysEmitIntoClient +internal var _SF_SNAPSHOT: CInterop.FileFlags { UInt32(bitPattern: SF_SNAPSHOT) } +#endif + +#endif // !os(Windows) diff --git a/Sources/System/Internals/Exports.swift b/Sources/System/Internals/Exports.swift index c7d9944f..025aefae 100644 --- a/Sources/System/Internals/Exports.swift +++ b/Sources/System/Internals/Exports.swift @@ -5,7 +5,7 @@ Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information -*/ + */ // Internal wrappers and typedefs which help reduce #if littering in System's // code base. @@ -90,6 +90,34 @@ internal func system_strlen(_ s: UnsafeMutablePointer) -> Int { strlen(s) } +#if !os(Windows) +internal func system_stat(_ p: UnsafePointer, _ s: inout CInterop.Stat) -> Int32 { + stat(p, &s) +} +internal func system_lstat(_ p: UnsafePointer, _ s: inout CInterop.Stat) -> Int32 { + lstat(p, &s) +} +internal func system_fstat(_ fd: CInt, _ s: inout CInterop.Stat) -> Int32 { + fstat(fd, &s) +} +internal func system_fstatat(_ fd: CInt, _ p: UnsafePointer, _ s: inout CInterop.Stat, _ flags: CInt) -> Int32 { + fstatat(fd, p, &s, flags) +} + +@usableFromInline +internal func system_major(_ dev: CInterop.DeviceID) -> CInt { + numericCast((dev >> 24) & 0xff) +} +@usableFromInline +internal func system_minor(_ dev: CInterop.DeviceID) -> CInt { + numericCast(dev & 0xffffff) +} +@usableFromInline +internal func system_makedev(_ maj: CUnsignedInt, _ min: CUnsignedInt) -> CInterop.DeviceID { + CInterop.DeviceID((maj << 24) | min) +} +#endif + // Convention: `system_platform_foo` is a // platform-representation-abstracted wrapper around `foo`-like functionality. // Type and layout differences such as the `char` vs `wchar` are abstracted. @@ -167,20 +195,20 @@ internal typealias _PlatformTLSKey = DWORD #elseif os(WASI) && (swift(<6.1) || !_runtime(_multithreaded)) // Mock TLS storage for single-threaded WASI internal final class _PlatformTLSKey { - fileprivate init() {} + fileprivate init() {} } private final class TLSStorage: @unchecked Sendable { - var storage = [ObjectIdentifier: UnsafeMutableRawPointer]() + var storage = [ObjectIdentifier: UnsafeMutableRawPointer]() } private let sharedTLSStorage = TLSStorage() func pthread_setspecific(_ key: _PlatformTLSKey, _ p: UnsafeMutableRawPointer?) -> Int { - sharedTLSStorage.storage[ObjectIdentifier(key)] = p - return 0 + sharedTLSStorage.storage[ObjectIdentifier(key)] = p + return 0 } func pthread_getspecific(_ key: _PlatformTLSKey) -> UnsafeMutableRawPointer? { - sharedTLSStorage.storage[ObjectIdentifier(key)] + sharedTLSStorage.storage[ObjectIdentifier(key)] } #else internal typealias _PlatformTLSKey = pthread_key_t diff --git a/Tests/SystemTests/FileModeTests.swift b/Tests/SystemTests/FileModeTests.swift new file mode 100644 index 00000000..ca010335 --- /dev/null +++ b/Tests/SystemTests/FileModeTests.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) + +import Testing + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@Suite("FileMode") +private struct FileModeTests { + + @Test func basics() async throws { + var mode = FileMode(rawValue: S_IFREG | 0o644) // Regular file, rw-r--r-- + #expect(mode.type == .regular) + #expect(mode.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + + mode.type = .directory // Directory, rw-r--r-- + #expect(mode.type == .directory) + #expect(mode.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + + mode.permissions.insert([.ownerExecute, .groupExecute, .otherExecute]) // Directory, rwxr-xr-x + #expect(mode.type == .directory) + #expect(mode.permissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]) + + mode.type = .symbolicLink // Symbolic link, rwxr-xr-x + #expect(mode.type == .symbolicLink) + #expect(mode.permissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]) + + let mode1 = FileMode(rawValue: S_IFLNK | 0o755) // Symbolic link, rwxr-xr-x + let mode2 = FileMode(type: .symbolicLink, permissions: [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]) + #expect(mode == mode1) + #expect(mode1 == mode2) + + mode.permissions.remove([.otherReadExecute]) // Symbolic link, rwxr-x--- + #expect(mode.permissions == [.ownerReadWriteExecute, .groupReadExecute]) + #expect(mode != mode1) + #expect(mode != mode2) + #expect(mode.type == mode1.type) + #expect(mode.type == mode2.type) + } + + @Test func invalidInput() async throws { + // No permissions, all other bits set + var invalidMode = FileMode(rawValue: ~0o7777) + #expect(invalidMode.permissions.isEmpty) + #expect(invalidMode.type != .directory) + #expect(invalidMode.type != .characterSpecial) + #expect(invalidMode.type != .blockSpecial) + #expect(invalidMode.type != .regular) + #expect(invalidMode.type != .fifo) + #expect(invalidMode.type != .symbolicLink) + #expect(invalidMode.type != .socket) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(invalidMode.type != .whiteout) + #endif + + // All file-type bits set + invalidMode = FileMode(rawValue: S_IFMT) + #expect(invalidMode.type != .directory) + #expect(invalidMode.type != .characterSpecial) + #expect(invalidMode.type != .blockSpecial) + #expect(invalidMode.type != .regular) + #expect(invalidMode.type != .fifo) + #expect(invalidMode.type != .symbolicLink) + #expect(invalidMode.type != .socket) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(invalidMode.type != .whiteout) + #endif + + // FileMode(type:permissions:) masks its inputs so + // they don't accidentally modify the other bits. + let emptyPermissions = FileMode(type: FileType(rawValue: ~0), permissions: []) + #expect(emptyPermissions.permissions.isEmpty) + #expect(emptyPermissions.type == FileType(rawValue: S_IFMT)) + #expect(emptyPermissions == invalidMode) + + let regularFile = FileMode(type: .regular, permissions: FilePermissions(rawValue: ~0)) + #expect(regularFile.type == .regular) + #expect(regularFile.permissions == FilePermissions(rawValue: 0o7777)) + #expect(regularFile.permissions == [ + .ownerReadWriteExecute, + .groupReadWriteExecute, + .otherReadWriteExecute, + .setUserID, .setGroupID, .saveText + ]) + + // Setting properties should not modify the other bits, either. + var mode = FileMode(rawValue: 0) + mode.type = FileType(rawValue: ~0) + #expect(mode.type == FileType(rawValue: S_IFMT)) + #expect(mode.permissions.isEmpty) + + mode.type.rawValue = 0 + #expect(mode.type == FileType(rawValue: 0)) + #expect(mode.permissions.isEmpty) + + mode.permissions = FilePermissions(rawValue: ~0) + #expect(mode.permissions == FilePermissions(rawValue: 0o7777)) + #expect(mode.type == FileType(rawValue: 0)) + + mode.permissions = [] + #expect(mode.permissions.isEmpty) + #expect(mode.type == FileType(rawValue: 0)) + } + +} +#endif diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift new file mode 100644 index 00000000..4fefe7cf --- /dev/null +++ b/Tests/SystemTests/StatTests.swift @@ -0,0 +1,424 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) + +import Testing + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import CSystem +import Glibc +#elseif canImport(Musl) +import CSystem +import Musl +#elseif canImport(WASILibc) +import CSystem +import WASILibc +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@Suite("Stat") +private struct StatTests { + + @Test func basics() async throws { + try withTemporaryFilePath(basename: "Stat_basics") { tempDir in + let dirStatFromFilePath = try tempDir.stat() + #expect(dirStatFromFilePath.type == .directory) + + let dirFD = try FileDescriptor.open(tempDir, .readOnly) + defer { + try? dirFD.close() + } + let dirStatFromFD = try dirFD.stat() + #expect(dirStatFromFD.type == .directory) + + let dirStatFromCString = try tempDir.withPlatformString { try Stat($0) } + #expect(dirStatFromCString.type == .directory) + + #expect(dirStatFromFilePath == dirStatFromFD) + #expect(dirStatFromFD == dirStatFromCString) + + let tempFile = tempDir.appending("test.txt") + let fileFD = try FileDescriptor.open(tempFile, .readWrite, options: .create, permissions: [.ownerReadWrite, .groupRead, .otherRead]) + defer { + try? fileFD.close() + } + try fileFD.writeAll("Hello, world!".utf8) + + let fileStatFromFD = try fileFD.stat() + #expect(fileStatFromFD.type == .regular) + #expect(fileStatFromFD.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + #expect(fileStatFromFD.size == "Hello, world!".utf8.count) + + let fileStatFromFilePath = try tempFile.stat() + #expect(fileStatFromFilePath.type == .regular) + + let fileStatFromCString = try tempFile.withPlatformString { try Stat($0) } + #expect(fileStatFromCString.type == .regular) + + #expect(fileStatFromFD == fileStatFromFilePath) + #expect(fileStatFromFilePath == fileStatFromCString) + } + } + + @Test + func followSymlinkInits() async throws { + try withTemporaryFilePath(basename: "Stat_followSymlinkInits") { tempDir in + let targetFilePath = tempDir.appending("target.txt") + let symlinkPath = tempDir.appending("symlink") + let targetFD = try FileDescriptor.open(targetFilePath, .readWrite, options: .create, permissions: .ownerReadWrite) + defer { + try? targetFD.close() + } + try targetFD.writeAll(Array(repeating: UInt8(ascii: "A"), count: 1025)) + + try targetFilePath.withPlatformString { targetPtr in + try symlinkPath.withPlatformString { symlinkPtr in + try #require(symlink(targetPtr, symlinkPtr) == 0, "\(Errno.current)") + } + } + + // Can't open an fd to a symlink on WASI (no O_PATH) + // On non-Darwin, we need O_PATH | O_NOFOLLOW to open the symlink + // directly, but O_PATH requires _GNU_SOURCE be defined (TODO). + #if SYSTEM_PACKAGE_DARWIN + let symlinkFD = try FileDescriptor.open(symlinkPath, .readOnly, options: .symlink) + defer { + try? symlinkFD.close() + } + #endif + + let targetStat = try targetFilePath.stat() + let originalTargetAccessTime = targetStat.st_atim + + let symlinkStat = try symlinkPath.stat(followTargetSymlink: false) + let originalSymlinkAccessTime = symlinkStat.st_atim + + #expect(targetStat != symlinkStat) + #expect(targetStat.type == .regular) + #expect(symlinkStat.type == .symbolicLink) + #expect(symlinkStat.size < targetStat.size) + #expect(symlinkStat.sizeAllocated < targetStat.sizeAllocated) + + // Set each .st_atim back to its original value for comparison + + // FileDescriptor Extensions + + var stat = try targetFD.stat() + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + #if SYSTEM_PACKAGE_DARWIN + stat = try symlinkFD.stat() + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + #endif + + // Initializing Stat with FileDescriptor + + stat = try Stat(targetFD) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + #if SYSTEM_PACKAGE_DARWIN + stat = try Stat(symlinkFD) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + #endif + + // FilePath Extensions + + stat = try symlinkPath.stat(followTargetSymlink: true) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try symlinkPath.stat(followTargetSymlink: false) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + + // Initializing Stat with UnsafePointer + + try symlinkPath.withPlatformString { pathPtr in + stat = try Stat(pathPtr, followTargetSymlink: true) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(pathPtr, followTargetSymlink: false) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + } + + // Initializing Stat with FilePath + + stat = try Stat(symlinkPath, followTargetSymlink: true) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(symlinkPath, followTargetSymlink: false) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + + // Initializing Stat with String + + stat = try Stat(symlinkPath.string, followTargetSymlink: true) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(symlinkPath.string, followTargetSymlink: false) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + } + } + + @Test func permissions() async throws { + try withTemporaryFilePath(basename: "Stat_permissions") { tempDir in + let testFile = tempDir.appending("test.txt") + let fd = try FileDescriptor.open(testFile, .writeOnly, options: .create, permissions: [.ownerReadWrite, .groupRead, .otherRead]) + try fd.close() + + let stat = try testFile.stat() + #expect(stat.type == .regular) + #expect(stat.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + + var newMode = stat.mode + newMode.permissions.insert(.ownerExecute) + try testFile.withPlatformString { pathPtr in + try #require(chmod(pathPtr, newMode.permissions.rawValue) == 0, "\(Errno.current)") + } + + let updatedStat = try testFile.stat() + #expect(updatedStat.permissions == newMode.permissions) + + newMode.permissions.remove(.ownerWriteExecute) + try testFile.withPlatformString { pathPtr in + try #require(chmod(pathPtr, newMode.permissions.rawValue) == 0, "\(Errno.current)") + } + + let readOnlyStat = try testFile.stat() + #expect(readOnlyStat.permissions == newMode.permissions) + } + } + + @Test + func times() async throws { + var start = timespec() + try #require(clock_gettime(CLOCK_REALTIME, &start) == 0, "\(Errno.current)") + start.tv_sec -= 1 // A little wiggle room + try withTemporaryFilePath(basename: "Stat_times") { tempDir in + var dirStat = try tempDir.stat() + let dirAccessTime0 = dirStat.st_atim + let dirModificationTime0 = dirStat.st_mtim + let dirChangeTime0 = dirStat.st_ctim + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let dirCreationTime0 = dirStat.st_birthtim + #endif + + var startUpperBound = start + startUpperBound.tv_sec += 5 + #expect(dirAccessTime0 >= start) + #expect(dirAccessTime0 < startUpperBound) + #expect(dirModificationTime0 >= start) + #expect(dirModificationTime0 < startUpperBound) + #expect(dirChangeTime0 >= start) + #expect(dirChangeTime0 < startUpperBound) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirCreationTime0 >= start) + #expect(dirCreationTime0 < startUpperBound) + #endif + + // Fails intermittently if less than 5ms + usleep(10000) + + let file1 = tempDir.appending("test1.txt") + let fd1 = try FileDescriptor.open(file1, .writeOnly, options: .create, permissions: .ownerReadWrite) + defer { + try? fd1.close() + } + + dirStat = try tempDir.stat() + let dirAccessTime1 = dirStat.st_atim + let dirModificationTime1 = dirStat.st_mtim + let dirChangeTime1 = dirStat.st_ctim + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let dirCreationTime1 = dirStat.st_birthtim + #endif + + // Creating a file updates directory modification and change time. + // Access time may not be updated depending on mount options like NOATIME. + + #expect(dirModificationTime1 > dirModificationTime0) + #expect(dirChangeTime1 > dirChangeTime0) + #expect(dirAccessTime1 >= dirAccessTime0) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirCreationTime1 == dirCreationTime0) + #endif + + usleep(10000) + + // Changing permissions only updates directory change time + + try tempDir.withPlatformString { pathPtr in + var newMode = dirStat.mode + // tempDir only starts with .ownerReadWriteExecute + newMode.permissions.insert(.groupReadWriteExecute) + try #require(chmod(pathPtr, newMode.rawValue) == 0, "\(Errno.current)") + } + + dirStat = try tempDir.stat() + let dirChangeTime2 = dirStat.st_ctim + #expect(dirChangeTime2 > dirChangeTime1) + #expect(dirStat.st_atim == dirAccessTime1) + #expect(dirStat.st_mtim == dirModificationTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirStat.st_birthtim == dirCreationTime1) + #endif + + var stat1 = try file1.stat() + let file1AccessTime1 = stat1.st_atim + let file1ModificationTime1 = stat1.st_mtim + let file1ChangeTime1 = stat1.st_ctim + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let file1CreationTime1 = stat1.st_birthtim + #endif + + usleep(10000) + + try fd1.writeAll("Hello, world!".utf8) + stat1 = try file1.stat() + let file1AccessTime2 = stat1.st_atim + let file1ModificationTime2 = stat1.st_mtim + let file1ChangeTime2 = stat1.st_ctim + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let file1CreationTime2 = stat1.st_birthtim + #endif + + #expect(file1AccessTime2 >= file1AccessTime1) + #expect(file1ModificationTime2 > file1ModificationTime1) + #expect(file1ChangeTime2 > file1ChangeTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(file1CreationTime2 == file1CreationTime1) + #endif + + // Changing file metadata or content does not update directory times + + dirStat = try tempDir.stat() + #expect(dirStat.st_ctim == dirChangeTime2) + #expect(dirStat.st_atim == dirAccessTime1) + #expect(dirStat.st_mtim == dirModificationTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirStat.st_birthtim == dirCreationTime1) + #endif + + usleep(10000) + + let file2 = tempDir.appending("test2.txt") + let fd2 = try FileDescriptor.open(file2, .writeOnly, options: .create, permissions: .ownerReadWrite) + defer { + try? fd2.close() + } + + let stat2 = try file2.stat() + #expect(stat2.st_atim > file1AccessTime2) + #expect(stat2.st_mtim > file1ModificationTime2) + #expect(stat2.st_ctim > file1ChangeTime2) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(stat2.st_birthtim > file1CreationTime2) + #endif + } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + @Test func flags() async throws { + try withTemporaryFilePath(basename: "Stat_flags") { tempDir in + let filePath = tempDir.appending("test.txt") + let fd = try FileDescriptor.open(filePath, .writeOnly, options: .create, permissions: .ownerReadWrite) + defer { + try? fd.close() + } + var stat = try fd.stat() + var flags = stat.flags + + #if SYSTEM_PACKAGE_DARWIN + let userSettableFlags: FileFlags = [ + .noDump, .userImmutable, .userAppend, + .opaque, .tracked, .hidden, + /* .dataVault (throws EPERM when testing) */ + ] + #elseif os(FreeBSD) + let userSettableFlags: FileFlags = [ + .noDump, .userImmutable, .userAppend, + .opaque, .tracked, .hidden, + .userNoUnlink, + .offline, + .readOnly, + .reparse, + .sparse, + .system + ] + #else // os(OpenBSD) + let userSettableFlags: FileFlags = [ + .noDump, .userImmutable, .userAppend + ] + #endif + + flags.insert(userSettableFlags) + try #require(fchflags(fd.rawValue, flags.rawValue) == 0, "\(Errno.current)") + + stat = try fd.stat() + #expect(stat.flags == flags) + + flags.remove(userSettableFlags) + try #require(fchflags(fd.rawValue, flags.rawValue) == 0, "\(Errno.current)") + + stat = try fd.stat() + #expect(stat.flags == flags) + } + } + #endif + +} + +// TODO: Re-enable for testing when _GNU_SOURCE can be defined. +//#if !SYSTEM_PACKAGE_DARWIN && !os(WASI) +//private extension FileDescriptor.OpenOptions { +// static var path: Self { Self(rawValue: O_PATH) } +//} +//#endif + +// Comparison operators for timespec until UTCClock.Instant properties are available +private func >= (lhs: timespec, rhs: timespec) -> Bool { + (lhs.tv_sec, lhs.tv_nsec) >= (rhs.tv_sec, rhs.tv_nsec) +} + +private func < (lhs: timespec, rhs: timespec) -> Bool { + (lhs.tv_sec, lhs.tv_nsec) < (rhs.tv_sec, rhs.tv_nsec) +} + +private func > (lhs: timespec, rhs: timespec) -> Bool { + (lhs.tv_sec, lhs.tv_nsec) > (rhs.tv_sec, rhs.tv_nsec) +} + +private func == (lhs: timespec, rhs: timespec) -> Bool { + lhs.tv_sec == rhs.tv_sec && lhs.tv_nsec == rhs.tv_nsec +} + +#endif