From 57d515e1d0671a252415e3631f6ac24a55093345 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 12 Aug 2025 12:09:37 -0600 Subject: [PATCH 01/12] Stat implementation --- Package.swift | 1 + Sources/System/FileSystem/FileFlags.swift | 254 +++++++ Sources/System/FileSystem/FileMode.swift | 54 ++ Sources/System/FileSystem/FileType.swift | 102 +++ Sources/System/FileSystem/Identifiers.swift | 89 +++ Sources/System/FileSystem/Stat.swift | 700 ++++++++++++++++++++ Sources/System/Internals/CInterop.swift | 10 +- Sources/System/Internals/Constants.swift | 152 ++++- Sources/System/Internals/Exports.swift | 40 +- Tests/SystemTests/FileModeTests.swift | 134 ++++ Tests/SystemTests/StatTests.swift | 408 ++++++++++++ 11 files changed, 1935 insertions(+), 9 deletions(-) create mode 100644 Sources/System/FileSystem/FileFlags.swift create mode 100644 Sources/System/FileSystem/FileMode.swift create mode 100644 Sources/System/FileSystem/FileType.swift create mode 100644 Sources/System/FileSystem/Identifiers.swift create mode 100644 Sources/System/FileSystem/Stat.swift create mode 100644 Tests/SystemTests/FileModeTests.swift create mode 100644 Tests/SystemTests/StatTests.swift diff --git a/Package.swift b/Package.swift index 11a43f61..8aba3315 100644 --- a/Package.swift +++ b/Package.swift @@ -87,6 +87,7 @@ let swiftSettings = swiftSettingsAvailability + swiftSettingsCI + [ let cSettings: [CSetting] = [ .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), + .define("_GNU_SOURCE", .when(platforms: [.linux])), ] #if SYSTEM_ABI_STABLE diff --git a/Sources/System/FileSystem/FileFlags.swift b/Sources/System/FileSystem/FileFlags.swift new file mode 100644 index 00000000..f89cbc15 --- /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 | +// | compressed | UF_COMPRESSED | UF_COMPRESSED | N/A | +// | tracked | UF_TRACKED | UF_TRACKED | N/A | +// | hidden | UF_HIDDEN | UF_HIDDEN | N/A | +// | restricted | SF_RESTRICTED | SF_RESTRICTED | N/A | +// | systemNoUnlink | SF_NOUNLINK | SF_NOUNLINK | N/A | +// | dataVault | UF_DATAVAULT | 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 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 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 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 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 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 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..14ae30ea --- /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 `0o7777`. + @_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..91718880 --- /dev/null +++ b/Sources/System/FileSystem/FileType.swift @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// 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 | +// | pipe | 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 value. + /// + /// - Note: `rawValue` should only contain the mode's file-type bits. Otherwise, + /// use `FileMode(rawValue:)` 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 pipe) + /// + /// The corresponding C constant is `S_IFIFO`. + @_alwaysEmitIntoClient + public static var pipe: 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..df9a01ae --- /dev/null +++ b/Sources/System/FileSystem/Identifiers.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// 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 `GroupID` 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 } +} + +/// 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 `DeviceID` from the given major and minor device numbers. + /// + /// The corresponding C function is `makedev()`. + @_alwaysEmitIntoClient + public 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 + public var major: CInt { system_major(rawValue) } + + /// The minor device number + /// + /// The corresponding C function is `minor()`. + @_alwaysEmitIntoClient + public 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 } +} +#endif // !os(Windows) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift new file mode 100644 index 00000000..76f37b0a --- /dev/null +++ b/Sources/System/FileSystem/Stat.swift @@ -0,0 +1,700 @@ +//===----------------------------------------------------------------------===// +// +// 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 SYSTEM_PACKAGE_DARWIN || 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 + public static var resolveBeneath: Flags { Flags(rawValue: _AT_RESOLVE_BENEATH) } + #endif + + #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 + @_alwaysEmitIntoClient + public var type: FileType { + get { mode.type } + set { + var newMode = mode + newMode.type = newValue + mode = newMode + } + } + + /// File permissions for the given mode + @_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 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 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 + /// + /// - Note: Calculated as `512 * blocksAllocated`. + @_alwaysEmitIntoClient + public var sizeAllocated: Int64 { + 512 * blocksAllocated + } + + // TODO: jflat - Change time properties to UTCClock.Instant when possible. + + /// Time of last access, given as a `Duration` since the Epoch + /// + /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var accessTime: Duration { + get { + #if SYSTEM_PACKAGE_DARWIN + let timespec = rawValue.st_atimespec + #else + let timespec = rawValue.st_atim + #endif + return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) + } + set { + let (seconds, attoseconds) = newValue.components + let timespec = timespec( + tv_sec: numericCast(seconds), + tv_nsec: numericCast(attoseconds / 1_000_000_000) + ) + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_atimespec = timespec + #else + rawValue.st_atim = timespec + #endif + } + } + + /// Time of last modification, given as a `Duration` since the Epoch + /// + /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var modificationTime: Duration { + get { + #if SYSTEM_PACKAGE_DARWIN + let timespec = rawValue.st_mtimespec + #else + let timespec = rawValue.st_mtim + #endif + return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) + } + set { + let (seconds, attoseconds) = newValue.components + let timespec = timespec( + tv_sec: numericCast(seconds), + tv_nsec: numericCast(attoseconds / 1_000_000_000) + ) + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_mtimespec = timespec + #else + rawValue.st_mtim = timespec + #endif + } + } + + /// Time of last status (inode) change, given as a `Duration` since the Epoch + /// + /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var changeTime: Duration { + get { + #if SYSTEM_PACKAGE_DARWIN + let timespec = rawValue.st_ctimespec + #else + let timespec = rawValue.st_ctim + #endif + return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) + } + set { + let (seconds, attoseconds) = newValue.components + let timespec = timespec( + tv_sec: numericCast(seconds), + tv_nsec: numericCast(attoseconds / 1_000_000_000) + ) + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_ctimespec = timespec + #else + rawValue.st_ctim = timespec + #endif + } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Time of file creation, given as a `Duration` since the Epoch + /// + /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). + /// - Note: Only available on Darwin and FreeBSD. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var creationTime: Duration { + get { + #if SYSTEM_PACKAGE_DARWIN + let timespec = rawValue.st_birthtimespec + #else + let timespec = rawValue.st_birthtim + #endif + return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) + } + set { + let (seconds, attoseconds) = newValue.components + let timespec = timespec( + tv_sec: numericCast(seconds), + tv_nsec: numericCast(attoseconds / 1_000_000_000) + ) + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_birthtimespec = timespec + #else + rawValue.st_birthtim = timespec + #endif + } + } + #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 + +// TODO: jflat + +// 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..8805ffad 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,150 @@ 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 SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } +#endif + +#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_COMPRESSED: CInterop.FileFlags { UInt32(bitPattern: UF_COMPRESSED) } + +@_alwaysEmitIntoClient +internal var _UF_TRACKED: CInterop.FileFlags { UInt32(bitPattern: UF_TRACKED) } + +@_alwaysEmitIntoClient +internal var _UF_HIDDEN: CInterop.FileFlags { UInt32(bitPattern: UF_HIDDEN) } + +@_alwaysEmitIntoClient +internal var _SF_RESTRICTED: CInterop.FileFlags { UInt32(bitPattern: SF_RESTRICTED) } + +@_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_DATAVAULT: CInterop.FileFlags { UInt32(bitPattern: UF_DATAVAULT) } + +@_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..bc302a71 --- /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 != .pipe) + #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 != .pipe) + #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..af93f6ea --- /dev/null +++ b/Tests/SystemTests/StatTests.swift @@ -0,0 +1,408 @@ +//===----------------------------------------------------------------------===// +// +// 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 + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + 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)") + } + } + + #if !os(WASI) // Can't open an fd to a symlink on WASI (no O_PATH) + #if SYSTEM_PACKAGE_DARWIN + let symlinkFD = try FileDescriptor.open(symlinkPath, .readOnly, options: .symlink) + #else + // Need O_PATH | O_NOFOLLOW to open the symlink directly + let symlinkFD = try FileDescriptor.open(symlinkPath, .readOnly, options: [.path, .noFollow]) + #endif + defer { + try? symlinkFD.close() + } + #endif // !os(WASI) + + let targetStat = try targetFilePath.stat() + let originalTargetAccessTime = targetStat.accessTime + + let symlinkStat = try symlinkPath.stat(followTargetSymlink: false) + let originalSymlinkAccessTime = symlinkStat.accessTime + + #expect(targetStat != symlinkStat) + #expect(targetStat.type == .regular) + #expect(symlinkStat.type == .symbolicLink) + #expect(symlinkStat.size < targetStat.size) + #expect(symlinkStat.sizeAllocated < targetStat.sizeAllocated) + + // Set each .accessTime back to its original value for comparison + + // FileDescriptor Extensions + + var stat = try targetFD.stat() + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + #if !os(WASI) + stat = try symlinkFD.stat() + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + #endif + + // Initializing Stat with FileDescriptor + + stat = try Stat(targetFD) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + #if !os(WASI) + stat = try Stat(symlinkFD) + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + #endif + + // FilePath Extensions + + stat = try symlinkPath.stat(followTargetSymlink: true) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try symlinkPath.stat(followTargetSymlink: false) + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + + // Initializing Stat with UnsafePointer + + try symlinkPath.withPlatformString { pathPtr in + stat = try Stat(pathPtr, followTargetSymlink: true) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(pathPtr, followTargetSymlink: false) + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + } + + // Initializing Stat with FilePath + + stat = try Stat(symlinkPath, followTargetSymlink: true) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(symlinkPath, followTargetSymlink: false) + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + + // Initializing Stat with String + + stat = try Stat(symlinkPath.string, followTargetSymlink: true) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(symlinkPath.string, followTargetSymlink: false) + stat.accessTime = 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 + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + func times() async throws { + let startTime = Int64(time(nil)) + try #require(startTime >= 0, "\(Errno.current)") + let start: Duration = .seconds(startTime - 1) // A little wiggle room + try withTemporaryFilePath(basename: "Stat_times") { tempDir in + var dirStat = try tempDir.stat() + let dirAccessTime0 = dirStat.accessTime + let dirModificationTime0 = dirStat.modificationTime + let dirChangeTime0 = dirStat.changeTime + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let dirCreationTime0 = dirStat.creationTime + #endif + + #expect(dirAccessTime0 >= start) + #expect(dirAccessTime0 < start + .seconds(5)) + #expect(dirModificationTime0 >= start) + #expect(dirModificationTime0 < start + .seconds(5)) + #expect(dirChangeTime0 >= start) + #expect(dirChangeTime0 < start + .seconds(5)) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirCreationTime0 >= start) + #expect(dirCreationTime0 < start + .seconds(5)) + #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.accessTime + let dirModificationTime1 = dirStat.modificationTime + let dirChangeTime1 = dirStat.changeTime + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let dirCreationTime1 = dirStat.creationTime + #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.changeTime + #expect(dirChangeTime2 > dirChangeTime1) + #expect(dirStat.accessTime == dirAccessTime1) + #expect(dirStat.modificationTime == dirModificationTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirStat.creationTime == dirCreationTime1) + #endif + + var stat1 = try file1.stat() + let file1AccessTime1 = stat1.accessTime + let file1ModificationTime1 = stat1.modificationTime + let file1ChangeTime1 = stat1.changeTime + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let file1CreationTime1 = stat1.creationTime + #endif + + usleep(10000) + + try fd1.writeAll("Hello, world!".utf8) + stat1 = try file1.stat() + let file1AccessTime2 = stat1.accessTime + let file1ModificationTime2 = stat1.modificationTime + let file1ChangeTime2 = stat1.changeTime + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let file1CreationTime2 = stat1.creationTime + #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.changeTime == dirChangeTime2) + #expect(dirStat.accessTime == dirAccessTime1) + #expect(dirStat.modificationTime == dirModificationTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirStat.creationTime == 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.accessTime > file1AccessTime2) + #expect(stat2.modificationTime > file1ModificationTime2) + #expect(stat2.changeTime > file1ChangeTime2) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(stat2.creationTime > 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 + +} + +#if !SYSTEM_PACKAGE_DARWIN && !os(WASI) +private extension FileDescriptor.OpenOptions { + static var path: Self { Self(rawValue: O_PATH) } +} +#endif + +#endif From a07696ace3ea8ce5448543543943a2ec8e84ba04 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 26 Aug 2025 17:23:41 -0600 Subject: [PATCH 02/12] Fix AT_RESOLVE_BENEATH availability --- Sources/System/FileSystem/Stat.swift | 3 ++- Sources/System/Internals/Constants.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 76f37b0a..4e857273 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -76,12 +76,13 @@ public struct Stat: RawRepresentable, Sendable { public static var symlinkNoFollowAny: Flags { Flags(rawValue: _AT_SYMLINK_NOFOLLOW_ANY) } #endif - #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #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 diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 8805ffad..37b7da33 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -658,7 +658,7 @@ internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_FOLLOW } internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } #endif -#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +#if canImport(Darwin, _version: 346) || os(FreeBSD) @_alwaysEmitIntoClient internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } #endif From 905f9f842dc6567cc29bee7470f8769918b84bde Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 26 Aug 2025 17:27:22 -0600 Subject: [PATCH 03/12] Expose C timespec properties until UTCClock can be used --- Sources/System/FileSystem/Stat.swift | 135 ++++++++++++++++----------- Tests/SystemTests/StatTests.swift | 120 ++++++++++++++---------- 2 files changed, 150 insertions(+), 105 deletions(-) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 4e857273..9f2e5503 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -410,119 +410,146 @@ public struct Stat: RawRepresentable, Sendable { 512 * blocksAllocated } - // TODO: jflat - Change time properties to UTCClock.Instant when possible. - - /// Time of last access, given as a `Duration` since the Epoch + /// Time of last access, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var accessTime: Duration { + @_alwaysEmitIntoClient + public var st_atim: timespec { get { #if SYSTEM_PACKAGE_DARWIN - let timespec = rawValue.st_atimespec + rawValue.st_atimespec #else - let timespec = rawValue.st_atim + rawValue.st_atim #endif - return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) } set { - let (seconds, attoseconds) = newValue.components - let timespec = timespec( - tv_sec: numericCast(seconds), - tv_nsec: numericCast(attoseconds / 1_000_000_000) - ) #if SYSTEM_PACKAGE_DARWIN - rawValue.st_atimespec = timespec + rawValue.st_atimespec = newValue #else - rawValue.st_atim = timespec + rawValue.st_atim = newValue #endif } } - /// Time of last modification, given as a `Duration` since the Epoch + /// Time of last modification, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var modificationTime: Duration { + @_alwaysEmitIntoClient + public var st_mtim: timespec { get { #if SYSTEM_PACKAGE_DARWIN - let timespec = rawValue.st_mtimespec + rawValue.st_mtimespec #else - let timespec = rawValue.st_mtim + rawValue.st_mtim #endif - return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) } set { - let (seconds, attoseconds) = newValue.components - let timespec = timespec( - tv_sec: numericCast(seconds), - tv_nsec: numericCast(attoseconds / 1_000_000_000) - ) #if SYSTEM_PACKAGE_DARWIN - rawValue.st_mtimespec = timespec + rawValue.st_mtimespec = newValue #else - rawValue.st_mtim = timespec + rawValue.st_mtim = newValue #endif } } - /// Time of last status (inode) change, given as a `Duration` since the Epoch + /// 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). - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var changeTime: Duration { + @_alwaysEmitIntoClient + public var st_ctim: timespec { get { #if SYSTEM_PACKAGE_DARWIN - let timespec = rawValue.st_ctimespec + rawValue.st_ctimespec #else - let timespec = rawValue.st_ctim + rawValue.st_ctim #endif - return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) } set { - let (seconds, attoseconds) = newValue.components - let timespec = timespec( - tv_sec: numericCast(seconds), - tv_nsec: numericCast(attoseconds / 1_000_000_000) - ) #if SYSTEM_PACKAGE_DARWIN - rawValue.st_ctimespec = timespec + rawValue.st_ctimespec = newValue #else - rawValue.st_ctim = timespec + rawValue.st_ctim = newValue #endif } } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - /// Time of file creation, given as a `Duration` since the Epoch + /// 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. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var creationTime: Duration { + @_alwaysEmitIntoClient + public var st_birthtim: timespec { get { #if SYSTEM_PACKAGE_DARWIN - let timespec = rawValue.st_birthtimespec + rawValue.st_birthtimespec #else - let timespec = rawValue.st_birthtim + rawValue.st_birthtim #endif - return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) } set { - let (seconds, attoseconds) = newValue.components - let timespec = timespec( - tv_sec: numericCast(seconds), - tv_nsec: numericCast(attoseconds / 1_000_000_000) - ) #if SYSTEM_PACKAGE_DARWIN - rawValue.st_birthtimespec = timespec + rawValue.st_birthtimespec = newValue #else - rawValue.st_birthtim = timespec + rawValue.st_birthtim = newValue #endif } } #endif + // TODO: jflat - Change time properties to UTCClock.Instant when possible. + +// /// 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 /// diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift index af93f6ea..58c34413 100644 --- a/Tests/SystemTests/StatTests.swift +++ b/Tests/SystemTests/StatTests.swift @@ -111,10 +111,10 @@ private struct StatTests { #endif // !os(WASI) let targetStat = try targetFilePath.stat() - let originalTargetAccessTime = targetStat.accessTime + let originalTargetAccessTime = targetStat.st_atim let symlinkStat = try symlinkPath.stat(followTargetSymlink: false) - let originalSymlinkAccessTime = symlinkStat.accessTime + let originalSymlinkAccessTime = symlinkStat.st_atim #expect(targetStat != symlinkStat) #expect(targetStat.type == .regular) @@ -122,72 +122,72 @@ private struct StatTests { #expect(symlinkStat.size < targetStat.size) #expect(symlinkStat.sizeAllocated < targetStat.sizeAllocated) - // Set each .accessTime back to its original value for comparison + // Set each .st_atim back to its original value for comparison // FileDescriptor Extensions var stat = try targetFD.stat() - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) #if !os(WASI) stat = try symlinkFD.stat() - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) #endif // Initializing Stat with FileDescriptor stat = try Stat(targetFD) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) #if !os(WASI) stat = try Stat(symlinkFD) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) #endif // FilePath Extensions stat = try symlinkPath.stat(followTargetSymlink: true) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) stat = try symlinkPath.stat(followTargetSymlink: false) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) // Initializing Stat with UnsafePointer try symlinkPath.withPlatformString { pathPtr in stat = try Stat(pathPtr, followTargetSymlink: true) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) stat = try Stat(pathPtr, followTargetSymlink: false) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) } // Initializing Stat with FilePath stat = try Stat(symlinkPath, followTargetSymlink: true) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) stat = try Stat(symlinkPath, followTargetSymlink: false) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) // Initializing Stat with String stat = try Stat(symlinkPath.string, followTargetSymlink: true) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) stat = try Stat(symlinkPath.string, followTargetSymlink: false) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) } } @@ -222,29 +222,30 @@ private struct StatTests { } @Test - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func times() async throws { - let startTime = Int64(time(nil)) - try #require(startTime >= 0, "\(Errno.current)") - let start: Duration = .seconds(startTime - 1) // A little wiggle room + 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.accessTime - let dirModificationTime0 = dirStat.modificationTime - let dirChangeTime0 = dirStat.changeTime + let dirAccessTime0 = dirStat.st_atim + let dirModificationTime0 = dirStat.st_mtim + let dirChangeTime0 = dirStat.st_ctim #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - let dirCreationTime0 = dirStat.creationTime + let dirCreationTime0 = dirStat.st_birthtim #endif + var startUpperBound = start + startUpperBound.tv_sec += 5 #expect(dirAccessTime0 >= start) - #expect(dirAccessTime0 < start + .seconds(5)) + #expect(dirAccessTime0 < startUpperBound) #expect(dirModificationTime0 >= start) - #expect(dirModificationTime0 < start + .seconds(5)) + #expect(dirModificationTime0 < startUpperBound) #expect(dirChangeTime0 >= start) - #expect(dirChangeTime0 < start + .seconds(5)) + #expect(dirChangeTime0 < startUpperBound) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) #expect(dirCreationTime0 >= start) - #expect(dirCreationTime0 < start + .seconds(5)) + #expect(dirCreationTime0 < startUpperBound) #endif // Fails intermittently if less than 5ms @@ -257,11 +258,11 @@ private struct StatTests { } dirStat = try tempDir.stat() - let dirAccessTime1 = dirStat.accessTime - let dirModificationTime1 = dirStat.modificationTime - let dirChangeTime1 = dirStat.changeTime + let dirAccessTime1 = dirStat.st_atim + let dirModificationTime1 = dirStat.st_mtim + let dirChangeTime1 = dirStat.st_ctim #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - let dirCreationTime1 = dirStat.creationTime + let dirCreationTime1 = dirStat.st_birthtim #endif // Creating a file updates directory modification and change time. @@ -286,31 +287,31 @@ private struct StatTests { } dirStat = try tempDir.stat() - let dirChangeTime2 = dirStat.changeTime + let dirChangeTime2 = dirStat.st_ctim #expect(dirChangeTime2 > dirChangeTime1) - #expect(dirStat.accessTime == dirAccessTime1) - #expect(dirStat.modificationTime == dirModificationTime1) + #expect(dirStat.st_atim == dirAccessTime1) + #expect(dirStat.st_mtim == dirModificationTime1) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - #expect(dirStat.creationTime == dirCreationTime1) + #expect(dirStat.st_birthtim == dirCreationTime1) #endif var stat1 = try file1.stat() - let file1AccessTime1 = stat1.accessTime - let file1ModificationTime1 = stat1.modificationTime - let file1ChangeTime1 = stat1.changeTime + let file1AccessTime1 = stat1.st_atim + let file1ModificationTime1 = stat1.st_mtim + let file1ChangeTime1 = stat1.st_ctim #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - let file1CreationTime1 = stat1.creationTime + let file1CreationTime1 = stat1.st_birthtim #endif usleep(10000) try fd1.writeAll("Hello, world!".utf8) stat1 = try file1.stat() - let file1AccessTime2 = stat1.accessTime - let file1ModificationTime2 = stat1.modificationTime - let file1ChangeTime2 = stat1.changeTime + let file1AccessTime2 = stat1.st_atim + let file1ModificationTime2 = stat1.st_mtim + let file1ChangeTime2 = stat1.st_ctim #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - let file1CreationTime2 = stat1.creationTime + let file1CreationTime2 = stat1.st_birthtim #endif #expect(file1AccessTime2 >= file1AccessTime1) @@ -323,11 +324,11 @@ private struct StatTests { // Changing file metadata or content does not update directory times dirStat = try tempDir.stat() - #expect(dirStat.changeTime == dirChangeTime2) - #expect(dirStat.accessTime == dirAccessTime1) - #expect(dirStat.modificationTime == dirModificationTime1) + #expect(dirStat.st_ctim == dirChangeTime2) + #expect(dirStat.st_atim == dirAccessTime1) + #expect(dirStat.st_mtim == dirModificationTime1) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - #expect(dirStat.creationTime == dirCreationTime1) + #expect(dirStat.st_birthtim == dirCreationTime1) #endif usleep(10000) @@ -339,11 +340,11 @@ private struct StatTests { } let stat2 = try file2.stat() - #expect(stat2.accessTime > file1AccessTime2) - #expect(stat2.modificationTime > file1ModificationTime2) - #expect(stat2.changeTime > file1ChangeTime2) + #expect(stat2.st_atim > file1AccessTime2) + #expect(stat2.st_mtim > file1ModificationTime2) + #expect(stat2.st_ctim > file1ChangeTime2) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - #expect(stat2.creationTime > file1CreationTime2) + #expect(stat2.st_birthtim > file1CreationTime2) #endif } } @@ -405,4 +406,21 @@ private extension FileDescriptor.OpenOptions { } #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 From 2f5b12b854f5003a7b7950c00e79a15bbb5f7349 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 29 Sep 2025 11:59:27 -0600 Subject: [PATCH 04/12] Updates for proposal v3 --- Sources/System/FileSystem/FileFlags.swift | 48 +++++++++---------- Sources/System/FileSystem/FileMode.swift | 2 +- Sources/System/FileSystem/FileType.swift | 20 +++++--- Sources/System/FileSystem/Identifiers.swift | 53 ++++++++++++++------- Sources/System/FileSystem/Stat.swift | 34 ++++++++++--- Sources/System/Internals/Constants.swift | 18 +++---- Tests/SystemTests/FileModeTests.swift | 4 +- 7 files changed, 111 insertions(+), 68 deletions(-) diff --git a/Sources/System/FileSystem/FileFlags.swift b/Sources/System/FileSystem/FileFlags.swift index f89cbc15..fd714cb8 100644 --- a/Sources/System/FileSystem/FileFlags.swift +++ b/Sources/System/FileSystem/FileFlags.swift @@ -21,12 +21,12 @@ // | systemImmutable | SF_IMMUTABLE | SF_IMMUTABLE | SF_IMMUTABLE | // | systemAppend | SF_APPEND | SF_APPEND | SF_APPEND | // | opaque | UF_OPAQUE | UF_OPAQUE | N/A | -// | compressed | UF_COMPRESSED | UF_COMPRESSED | N/A | -// | tracked | UF_TRACKED | UF_TRACKED | N/A | // | hidden | UF_HIDDEN | UF_HIDDEN | N/A | -// | restricted | SF_RESTRICTED | SF_RESTRICTED | 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 | @@ -114,20 +114,6 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public static var opaque: FileFlags { FileFlags(rawValue: _UF_OPAQUE) } - /// 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 should not be displayed in a GUI. /// /// The corresponding C constant is `UF_HIDDEN`. @@ -135,13 +121,6 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public static var hidden: FileFlags { FileFlags(rawValue: _UF_HIDDEN) } - /// 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 may not be removed or renamed. /// /// The corresponding C constant is `SF_NOUNLINK`. @@ -153,6 +132,20 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { // 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`. @@ -160,6 +153,13 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { @_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 diff --git a/Sources/System/FileSystem/FileMode.swift b/Sources/System/FileSystem/FileMode.swift index 14ae30ea..9a099476 100644 --- a/Sources/System/FileSystem/FileMode.swift +++ b/Sources/System/FileSystem/FileMode.swift @@ -44,7 +44,7 @@ public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { /// The file's permissions, from the mode's permission bits. /// - /// Setting this property will mask the `newValue` with the permissions bit mask `0o7777`. + /// 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) } diff --git a/Sources/System/FileSystem/FileType.swift b/Sources/System/FileSystem/FileType.swift index 91718880..638b1472 100644 --- a/Sources/System/FileSystem/FileType.swift +++ b/Sources/System/FileSystem/FileType.swift @@ -18,7 +18,7 @@ // | characterSpecial | S_IFCHR | // | blockSpecial | S_IFBLK | // | regular | S_IFREG | -// | pipe | S_IFIFO | +// | fifo | S_IFIFO | // | symbolicLink | S_IFLNK | // | socket | S_IFSOCK | // |------------------|---------------------| @@ -41,11 +41,17 @@ public struct FileType: RawRepresentable, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public var rawValue: CInterop.Mode - /// Creates a strongly-typed file type from the raw C value. + /// Creates a strongly-typed file type from the raw C `mode_t`. /// - /// - Note: `rawValue` should only contain the mode's file-type bits. Otherwise, - /// use `FileMode(rawValue:)` to get a strongly-typed `FileMode`, then - /// call `.type` to get the properly masked `FileType`. + /// - 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 } @@ -73,11 +79,11 @@ public struct FileType: RawRepresentable, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public static var regular: FileType { FileType(rawValue: _S_IFREG) } - /// FIFO (or pipe) + /// FIFO (or named pipe) /// /// The corresponding C constant is `S_IFIFO`. @_alwaysEmitIntoClient - public static var pipe: FileType { FileType(rawValue: _S_IFIFO) } + public static var fifo: FileType { FileType(rawValue: _S_IFIFO) } /// Symbolic link /// diff --git a/Sources/System/FileSystem/Identifiers.swift b/Sources/System/FileSystem/Identifiers.swift index df9a01ae..7620b601 100644 --- a/Sources/System/FileSystem/Identifiers.swift +++ b/Sources/System/FileSystem/Identifiers.swift @@ -19,9 +19,13 @@ public struct UserID: RawRepresentable, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public var rawValue: CInterop.UserID - /// Creates a strongly-typed `GroupID` from the raw C value. + /// 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. @@ -36,6 +40,10 @@ public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { /// 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. @@ -51,26 +59,31 @@ public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public init(rawValue: CInterop.DeviceID) { self.rawValue = rawValue } - - /// Creates a `DeviceID` from the given major and minor device numbers. - /// - /// The corresponding C function is `makedev()`. + /// Creates a strongly-typed `DeviceID` from the raw C value. @_alwaysEmitIntoClient - public static func make(major: CUnsignedInt, minor: CUnsignedInt) -> DeviceID { - DeviceID(rawValue: system_makedev(major, minor)) - } + public init(_ rawValue: CInterop.DeviceID) { self.rawValue = rawValue } - /// The major device number - /// - /// The corresponding C function is `major()`. - @_alwaysEmitIntoClient - public var major: CInt { system_major(rawValue) } + // TODO: API review for ID wrapper functionality - /// The minor device number - /// - /// The corresponding C function is `minor()`. - @_alwaysEmitIntoClient - public var minor: CInt { system_minor(rawValue) } +// /// 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. @@ -85,5 +98,9 @@ public struct Inode: RawRepresentable, Sendable, Hashable, Codable { /// 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 index 9f2e5503..6bc78ab3 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -122,7 +122,7 @@ public struct Stat: RawRepresentable, Sendable { }.get() } - /// Creates a `Stat` struct from an`UnsafePointer` path. + /// 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()`. @@ -314,6 +314,9 @@ public struct Stat: RawRepresentable, Sendable { } /// 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 } @@ -325,6 +328,9 @@ public struct Stat: RawRepresentable, Sendable { } /// 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 } @@ -365,7 +371,7 @@ public struct Stat: RawRepresentable, Sendable { /// 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 + /// 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`. @@ -377,6 +383,12 @@ public struct Stat: RawRepresentable, Sendable { /// 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 { @@ -395,6 +407,9 @@ public struct Stat: RawRepresentable, Sendable { /// 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 { @@ -404,12 +419,19 @@ public struct Stat: RawRepresentable, Sendable { /// 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). @@ -497,7 +519,7 @@ public struct Stat: RawRepresentable, Sendable { } #endif - // TODO: jflat - Change time properties to UTCClock.Instant when possible. + // TODO: Investigate changing time properties to UTCClock.Instant once available. // /// Time of last access, given as a `UTCClock.Instant` // /// @@ -602,8 +624,6 @@ extension Stat: Hashable { // MARK: - CustomStringConvertible and CustomDebugStringConvertible -// TODO: jflat - // MARK: - FileDescriptor Extensions // @available(System X.Y.Z, *) @@ -673,7 +693,7 @@ extension FilePath { } } - /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`. + /// 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. /// @@ -690,7 +710,7 @@ extension FilePath { ).get() } - /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`, + /// 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. diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 37b7da33..c4fda718 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -732,18 +732,9 @@ internal var _SF_APPEND: CInterop.FileFlags { UInt32(bitPattern: SF_APPEND) } @_alwaysEmitIntoClient internal var _UF_OPAQUE: CInterop.FileFlags { UInt32(bitPattern: UF_OPAQUE) } -@_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_HIDDEN: CInterop.FileFlags { UInt32(bitPattern: UF_HIDDEN) } -@_alwaysEmitIntoClient -internal var _SF_RESTRICTED: CInterop.FileFlags { UInt32(bitPattern: SF_RESTRICTED) } - @_alwaysEmitIntoClient internal var _SF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: SF_NOUNLINK) } #endif @@ -751,9 +742,18 @@ internal var _SF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: SF_NOUNLINK) // 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) } diff --git a/Tests/SystemTests/FileModeTests.swift b/Tests/SystemTests/FileModeTests.swift index bc302a71..ca010335 100644 --- a/Tests/SystemTests/FileModeTests.swift +++ b/Tests/SystemTests/FileModeTests.swift @@ -74,7 +74,7 @@ private struct FileModeTests { #expect(invalidMode.type != .characterSpecial) #expect(invalidMode.type != .blockSpecial) #expect(invalidMode.type != .regular) - #expect(invalidMode.type != .pipe) + #expect(invalidMode.type != .fifo) #expect(invalidMode.type != .symbolicLink) #expect(invalidMode.type != .socket) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @@ -87,7 +87,7 @@ private struct FileModeTests { #expect(invalidMode.type != .characterSpecial) #expect(invalidMode.type != .blockSpecial) #expect(invalidMode.type != .regular) - #expect(invalidMode.type != .pipe) + #expect(invalidMode.type != .fifo) #expect(invalidMode.type != .symbolicLink) #expect(invalidMode.type != .socket) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) From 98712f67f23f3b411e81dba13d42ba3355956872 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 6 Oct 2025 13:18:14 -0600 Subject: [PATCH 05/12] Remove _GNU_SOURCE define and AT_EMPTY_PATH for now --- Package.swift | 1 - Sources/System/FileSystem/Stat.swift | 19 +++++++++-------- Sources/System/Internals/Constants.swift | 9 ++++---- Tests/SystemTests/StatTests.swift | 26 +++++++++++------------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Package.swift b/Package.swift index 8aba3315..11a43f61 100644 --- a/Package.swift +++ b/Package.swift @@ -87,7 +87,6 @@ let swiftSettings = swiftSettingsAvailability + swiftSettingsCI + [ let cSettings: [CSetting] = [ .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), - .define("_GNU_SOURCE", .when(platforms: [.linux])), ] #if SYSTEM_ABI_STABLE diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 6bc78ab3..31243e68 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -86,15 +86,16 @@ public struct Stat: RawRepresentable, Sendable { public static var resolveBeneath: Flags { Flags(rawValue: _AT_RESOLVE_BENEATH) } #endif - #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 + // 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 diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index c4fda718..3d4b7efd 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -663,10 +663,11 @@ internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } #endif -#if os(FreeBSD) || os(Linux) || os(Android) -@_alwaysEmitIntoClient -internal var _AT_EMPTY_PATH: CInt { AT_EMPTY_PATH } -#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 diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift index 58c34413..4fefe7cf 100644 --- a/Tests/SystemTests/StatTests.swift +++ b/Tests/SystemTests/StatTests.swift @@ -81,7 +81,6 @@ private struct StatTests { } @Test - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func followSymlinkInits() async throws { try withTemporaryFilePath(basename: "Stat_followSymlinkInits") { tempDir in let targetFilePath = tempDir.appending("target.txt") @@ -98,17 +97,15 @@ private struct StatTests { } } - #if !os(WASI) // Can't open an fd to a symlink on WASI (no O_PATH) + // 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) - #else - // Need O_PATH | O_NOFOLLOW to open the symlink directly - let symlinkFD = try FileDescriptor.open(symlinkPath, .readOnly, options: [.path, .noFollow]) - #endif defer { try? symlinkFD.close() } - #endif // !os(WASI) + #endif let targetStat = try targetFilePath.stat() let originalTargetAccessTime = targetStat.st_atim @@ -130,7 +127,7 @@ private struct StatTests { stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) - #if !os(WASI) + #if SYSTEM_PACKAGE_DARWIN stat = try symlinkFD.stat() stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) @@ -142,7 +139,7 @@ private struct StatTests { stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) - #if !os(WASI) + #if SYSTEM_PACKAGE_DARWIN stat = try Stat(symlinkFD) stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) @@ -400,11 +397,12 @@ private struct StatTests { } -#if !SYSTEM_PACKAGE_DARWIN && !os(WASI) -private extension FileDescriptor.OpenOptions { - static var path: Self { Self(rawValue: O_PATH) } -} -#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 { From 501bb7d052f5b41a6bbb0dad1db611f5b527cafc Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 28 Oct 2025 22:30:47 -0600 Subject: [PATCH 06/12] Remove commented-out code for future directions --- Sources/System/FileSystem/Identifiers.swift | 22 ------- Sources/System/FileSystem/Stat.swift | 64 --------------------- Sources/System/Internals/Exports.swift | 13 ----- 3 files changed, 99 deletions(-) diff --git a/Sources/System/FileSystem/Identifiers.swift b/Sources/System/FileSystem/Identifiers.swift index 7620b601..ca2835ea 100644 --- a/Sources/System/FileSystem/Identifiers.swift +++ b/Sources/System/FileSystem/Identifiers.swift @@ -62,28 +62,6 @@ public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { /// 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. diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 31243e68..f31fa5d7 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -85,17 +85,6 @@ public struct Stat: RawRepresentable, Sendable { @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 @@ -520,59 +509,6 @@ public struct Stat: RawRepresentable, Sendable { } #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 /// diff --git a/Sources/System/Internals/Exports.swift b/Sources/System/Internals/Exports.swift index 025aefae..58d0db80 100644 --- a/Sources/System/Internals/Exports.swift +++ b/Sources/System/Internals/Exports.swift @@ -103,19 +103,6 @@ internal func system_fstat(_ fd: CInt, _ s: inout CInterop.Stat) -> Int32 { 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 From 90dba1dc68343627ddd80a22dd7a9ed56cd7f72d Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 28 Oct 2025 22:31:34 -0600 Subject: [PATCH 07/12] AT_SYMLINK_FOLLOW -> AT_SYMLINK_NOFOLLOW --- Sources/System/Internals/Constants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 3d4b7efd..1d2030cf 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -651,7 +651,7 @@ internal var _AT_FDCWD: CInt { AT_FDCWD } // MARK: - fstatat Flags @_alwaysEmitIntoClient -internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_FOLLOW } +internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_NOFOLLOW } #if SYSTEM_PACKAGE_DARWIN @_alwaysEmitIntoClient From eee01456718b82983443be29523fca641827f9a2 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 28 Oct 2025 22:52:34 -0600 Subject: [PATCH 08/12] Standardize on "file system" --- Sources/System/Errno.swift | 2 +- Sources/System/FileSystem/Stat.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/System/Errno.swift b/Sources/System/Errno.swift index 43b46af5..94eb102d 100644 --- a/Sources/System/Errno.swift +++ b/Sources/System/Errno.swift @@ -961,7 +961,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// Stale NFS file handle. /// - /// You attempted access an open file on an NFS filesystem, + /// You attempted access an open file on an NFS file system, /// which is now unavailable as referenced by the given file descriptor. /// This may indicate that the file was deleted on the NFS server /// or that some other catastrophic event occurred. diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index f31fa5d7..e1d221bf 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -374,7 +374,7 @@ public struct Stat: RawRepresentable, Sendable { /// 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 + /// 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. @@ -386,7 +386,7 @@ public struct Stat: RawRepresentable, Sendable { set { rawValue.st_size = numericCast(newValue) } } - /// Block size for filesystem I/O, in bytes + /// Block size for file system I/O, in bytes /// /// The corresponding C property is `st_blksize`. @_alwaysEmitIntoClient @@ -398,7 +398,7 @@ public struct Stat: RawRepresentable, Sendable { /// 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. + /// which can have file-system–dependent behavior. /// /// The corresponding C property is `st_blocks`. @_alwaysEmitIntoClient @@ -410,7 +410,7 @@ public struct Stat: RawRepresentable, Sendable { /// 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. + /// which can have file-system–dependent behavior. /// /// - Note: Calculated as `512 * blocksAllocated`. @_alwaysEmitIntoClient From 40e1deb663e499309739adb6a2acf193ebd7aa0e Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Wed, 29 Oct 2025 11:47:32 -0600 Subject: [PATCH 09/12] Consolidate internal Stat functions --- Sources/System/FileSystem/Stat.swift | 88 ++++++---------------------- 1 file changed, 17 insertions(+), 71 deletions(-) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index e1d221bf..f8bdb602 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -103,7 +103,7 @@ public struct Stat: RawRepresentable, Sendable { followTargetSymlink: Bool = true, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try path.withPlatformString { + self = try path.withPlatformString { Self._stat( $0, followTargetSymlink: followTargetSymlink, @@ -126,7 +126,7 @@ public struct Stat: RawRepresentable, Sendable { followTargetSymlink: Bool = true, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try Self._stat( + self = try Self._stat( path, followTargetSymlink: followTargetSymlink, retryOnInterrupt: retryOnInterrupt @@ -138,7 +138,7 @@ public struct Stat: RawRepresentable, Sendable { _ ptr: UnsafePointer, followTargetSymlink: Bool, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { if followTargetSymlink { @@ -146,7 +146,7 @@ public struct Stat: RawRepresentable, Sendable { } else { system_lstat(ptr, &result) } - }.map { result } + }.map { Stat(rawValue: result) } } /// Creates a `Stat` struct from a `FileDescriptor`. @@ -157,7 +157,7 @@ public struct Stat: RawRepresentable, Sendable { _ fd: FileDescriptor, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try Self._fstat( + self = try Self._fstat( fd, retryOnInterrupt: retryOnInterrupt ).get() @@ -167,11 +167,11 @@ public struct Stat: RawRepresentable, Sendable { internal static func _fstat( _ fd: FileDescriptor, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { system_fstat(fd.rawValue, &result) - }.map { result } + }.map { Stat(rawValue: result) } } /// Creates a `Stat` struct from a `FilePath` and `Flags`. @@ -185,7 +185,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try path.withPlatformString { + self = try path.withPlatformString { Self._fstatat( $0, relativeTo: _AT_FDCWD, @@ -209,7 +209,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try path.withPlatformString { + self = try path.withPlatformString { Self._fstatat( $0, relativeTo: fd.rawValue, @@ -230,7 +230,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try Self._fstatat( + self = try Self._fstatat( path, relativeTo: _AT_FDCWD, flags: flags, @@ -252,7 +252,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try Self._fstatat( + self = try Self._fstatat( path, relativeTo: fd.rawValue, flags: flags, @@ -266,11 +266,11 @@ public struct Stat: RawRepresentable, Sendable { relativeTo fd: FileDescriptor.RawValue, flags: Stat.Flags, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { system_fstatat(fd, path, &result, flags.rawValue) - }.map { result } + }.map { Stat(rawValue: result) } } @@ -573,19 +573,7 @@ extension FileDescriptor { 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) } + try Stat(self, retryOnInterrupt: retryOnInterrupt) } } @@ -607,27 +595,7 @@ extension FilePath { 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) } - } + try Stat(self, followTargetSymlink: followTargetSymlink, retryOnInterrupt: retryOnInterrupt) } /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`. @@ -640,11 +608,7 @@ extension FilePath { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) -> Stat { - try _fstatat( - relativeTo: _AT_FDCWD, - flags: flags, - retryOnInterrupt: retryOnInterrupt - ).get() + try Stat(self, flags: flags, retryOnInterrupt: retryOnInterrupt) } /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`, @@ -660,25 +624,7 @@ extension FilePath { 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) } - } + try Stat(self, relativeTo: fd, flags: flags, retryOnInterrupt: retryOnInterrupt) } } From 4911642226096fed703dee029786abda0ef28ba5 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Wed, 29 Oct 2025 11:57:30 -0600 Subject: [PATCH 10/12] Add availability, fix CInterop availability issue on old Darwin platforms --- Package.swift | 2 + Sources/System/FileSystem/FileFlags.swift | 4 +- Sources/System/FileSystem/FileMode.swift | 2 +- Sources/System/FileSystem/FileType.swift | 2 +- Sources/System/FileSystem/Identifiers.swift | 8 +-- Sources/System/FileSystem/Stat.swift | 9 +-- Sources/System/Internals/Constants.swift | 64 ++++++++++----------- Tests/SystemTests/FileModeTests.swift | 2 + Tests/SystemTests/StatTests.swift | 11 ++-- 9 files changed, 56 insertions(+), 48 deletions(-) diff --git a/Package.swift b/Package.swift index 11a43f61..c7621c05 100644 --- a/Package.swift +++ b/Package.swift @@ -64,6 +64,8 @@ let availability: [Available] = [ Available("1.5.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), Available("1.6.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), Available("1.6.1", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + + Available("99", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), ] let swiftSettingsAvailability = availability.map(\.swiftSetting) diff --git a/Sources/System/FileSystem/FileFlags.swift b/Sources/System/FileSystem/FileFlags.swift index fd714cb8..8fb54938 100644 --- a/Sources/System/FileSystem/FileFlags.swift +++ b/Sources/System/FileSystem/FileFlags.swift @@ -39,7 +39,7 @@ // |------------------|---------------|---------------|---------------| #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) -// @available(System X.Y.Z, *) +@available(System 99, *) extension CInterop { public typealias FileFlags = UInt32 } @@ -49,7 +49,7 @@ extension CInterop { /// /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct FileFlags: OptionSet, Sendable, Hashable, Codable { /// The raw C flags. diff --git a/Sources/System/FileSystem/FileMode.swift b/Sources/System/FileSystem/FileMode.swift index 9a099476..91329f1a 100644 --- a/Sources/System/FileSystem/FileMode.swift +++ b/Sources/System/FileSystem/FileMode.swift @@ -14,7 +14,7 @@ /// /// - Note: Only available on Unix-like platforms. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { /// The raw C mode. diff --git a/Sources/System/FileSystem/FileType.swift b/Sources/System/FileSystem/FileType.swift index 638b1472..42134522 100644 --- a/Sources/System/FileSystem/FileType.swift +++ b/Sources/System/FileSystem/FileType.swift @@ -34,7 +34,7 @@ /// /// - Note: Only available on Unix-like platforms. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct FileType: RawRepresentable, Sendable, Hashable, Codable { /// The raw file-type bits from the C mode. diff --git a/Sources/System/FileSystem/Identifiers.swift b/Sources/System/FileSystem/Identifiers.swift index ca2835ea..b8f90141 100644 --- a/Sources/System/FileSystem/Identifiers.swift +++ b/Sources/System/FileSystem/Identifiers.swift @@ -12,7 +12,7 @@ #if !os(Windows) /// A Swift wrapper of the C `uid_t` type. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct UserID: RawRepresentable, Sendable, Hashable, Codable { /// The raw C `uid_t`. @@ -30,7 +30,7 @@ public struct UserID: RawRepresentable, Sendable, Hashable, Codable { /// A Swift wrapper of the C `gid_t` type. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { /// The raw C `gid_t`. @@ -48,7 +48,7 @@ public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { /// A Swift wrapper of the C `dev_t` type. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { /// The raw C `dev_t`. @@ -66,7 +66,7 @@ public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { /// A Swift wrapper of the C `ino_t` type. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct Inode: RawRepresentable, Sendable, Hashable, Codable { /// The raw C `ino_t`. diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index f8bdb602..98b7dc19 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -35,7 +35,7 @@ import Android /// /// - Note: Only available on Unix-like platforms. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct Stat: RawRepresentable, Sendable { /// The raw C `stat` struct. @@ -82,7 +82,6 @@ public struct Stat: RawRepresentable, Sendable { /// 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 } @@ -537,6 +536,7 @@ public struct Stat: RawRepresentable, Sendable { // MARK: - Equatable and Hashable +@available(System 99, *) extension Stat: Equatable { @_alwaysEmitIntoClient /// Compares the raw bytes of two `Stat` structs for equality. @@ -549,6 +549,7 @@ extension Stat: Equatable { } } +@available(System 99, *) extension Stat: Hashable { @_alwaysEmitIntoClient /// Hashes the raw bytes of this `Stat` struct. @@ -563,7 +564,7 @@ extension Stat: Hashable { // MARK: - FileDescriptor Extensions -// @available(System X.Y.Z, *) +@available(System 99, *) extension FileDescriptor { /// Creates a `Stat` struct for the file referenced by this `FileDescriptor`. @@ -579,7 +580,7 @@ extension FileDescriptor { // MARK: - FilePath Extensions -// @available(System X.Y.Z, *) +@available(System 99, *) extension FilePath { /// Creates a `Stat` struct for the file referenced by this `FilePath`. diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 1d2030cf..8740d81e 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -672,35 +672,35 @@ internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } // MARK: - File Mode / File Type @_alwaysEmitIntoClient -internal var _MODE_FILETYPE_MASK: CInterop.Mode { S_IFMT } +internal var _MODE_FILETYPE_MASK: mode_t { S_IFMT } @_alwaysEmitIntoClient -internal var _MODE_PERMISSIONS_MASK: CInterop.Mode { 0o7777 } +internal var _MODE_PERMISSIONS_MASK: mode_t { 0o7777 } @_alwaysEmitIntoClient -internal var _S_IFDIR: CInterop.Mode { S_IFDIR } +internal var _S_IFDIR: mode_t { S_IFDIR } @_alwaysEmitIntoClient -internal var _S_IFCHR: CInterop.Mode { S_IFCHR } +internal var _S_IFCHR: mode_t { S_IFCHR } @_alwaysEmitIntoClient -internal var _S_IFBLK: CInterop.Mode { S_IFBLK } +internal var _S_IFBLK: mode_t { S_IFBLK } @_alwaysEmitIntoClient -internal var _S_IFREG: CInterop.Mode { S_IFREG } +internal var _S_IFREG: mode_t { S_IFREG } @_alwaysEmitIntoClient -internal var _S_IFIFO: CInterop.Mode { S_IFIFO } +internal var _S_IFIFO: mode_t { S_IFIFO } @_alwaysEmitIntoClient -internal var _S_IFLNK: CInterop.Mode { S_IFLNK } +internal var _S_IFLNK: mode_t { S_IFLNK } @_alwaysEmitIntoClient -internal var _S_IFSOCK: CInterop.Mode { S_IFSOCK } +internal var _S_IFSOCK: mode_t { S_IFSOCK } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @_alwaysEmitIntoClient -internal var _S_IFWHT: CInterop.Mode { S_IFWHT } +internal var _S_IFWHT: mode_t { S_IFWHT } #endif // MARK: - stat/chflags File Flags @@ -709,82 +709,82 @@ internal var _S_IFWHT: CInterop.Mode { S_IFWHT } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) @_alwaysEmitIntoClient -internal var _UF_NODUMP: CInterop.FileFlags { UInt32(bitPattern: UF_NODUMP) } +internal var _UF_NODUMP: UInt32 { UInt32(bitPattern: UF_NODUMP) } @_alwaysEmitIntoClient -internal var _UF_IMMUTABLE: CInterop.FileFlags { UInt32(bitPattern: UF_IMMUTABLE) } +internal var _UF_IMMUTABLE: UInt32 { UInt32(bitPattern: UF_IMMUTABLE) } @_alwaysEmitIntoClient -internal var _UF_APPEND: CInterop.FileFlags { UInt32(bitPattern: UF_APPEND) } +internal var _UF_APPEND: UInt32 { UInt32(bitPattern: UF_APPEND) } @_alwaysEmitIntoClient -internal var _SF_ARCHIVED: CInterop.FileFlags { UInt32(bitPattern: SF_ARCHIVED) } +internal var _SF_ARCHIVED: UInt32 { UInt32(bitPattern: SF_ARCHIVED) } @_alwaysEmitIntoClient -internal var _SF_IMMUTABLE: CInterop.FileFlags { UInt32(bitPattern: SF_IMMUTABLE) } +internal var _SF_IMMUTABLE: UInt32 { UInt32(bitPattern: SF_IMMUTABLE) } @_alwaysEmitIntoClient -internal var _SF_APPEND: CInterop.FileFlags { UInt32(bitPattern: SF_APPEND) } +internal var _SF_APPEND: UInt32 { 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) } +internal var _UF_OPAQUE: UInt32 { UInt32(bitPattern: UF_OPAQUE) } @_alwaysEmitIntoClient -internal var _UF_HIDDEN: CInterop.FileFlags { UInt32(bitPattern: UF_HIDDEN) } +internal var _UF_HIDDEN: UInt32 { UInt32(bitPattern: UF_HIDDEN) } @_alwaysEmitIntoClient -internal var _SF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: SF_NOUNLINK) } +internal var _SF_NOUNLINK: UInt32 { 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) } +internal var _UF_COMPRESSED: UInt32 { UInt32(bitPattern: UF_COMPRESSED) } @_alwaysEmitIntoClient -internal var _UF_TRACKED: CInterop.FileFlags { UInt32(bitPattern: UF_TRACKED) } +internal var _UF_TRACKED: UInt32 { UInt32(bitPattern: UF_TRACKED) } @_alwaysEmitIntoClient -internal var _UF_DATAVAULT: CInterop.FileFlags { UInt32(bitPattern: UF_DATAVAULT) } +internal var _UF_DATAVAULT: UInt32 { UInt32(bitPattern: UF_DATAVAULT) } @_alwaysEmitIntoClient -internal var _SF_RESTRICTED: CInterop.FileFlags { UInt32(bitPattern: SF_RESTRICTED) } +internal var _SF_RESTRICTED: UInt32 { UInt32(bitPattern: SF_RESTRICTED) } @_alwaysEmitIntoClient -internal var _SF_FIRMLINK: CInterop.FileFlags { UInt32(bitPattern: SF_FIRMLINK) } +internal var _SF_FIRMLINK: UInt32 { UInt32(bitPattern: SF_FIRMLINK) } @_alwaysEmitIntoClient -internal var _SF_DATALESS: CInterop.FileFlags { UInt32(bitPattern: SF_DATALESS) } +internal var _SF_DATALESS: UInt32 { 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) } +internal var _UF_NOUNLINK: UInt32 { UInt32(bitPattern: UF_NOUNLINK) } @_alwaysEmitIntoClient -internal var _UF_OFFLINE: CInterop.FileFlags { UInt32(bitPattern: UF_OFFLINE) } +internal var _UF_OFFLINE: UInt32 { UInt32(bitPattern: UF_OFFLINE) } @_alwaysEmitIntoClient -internal var _UF_READONLY: CInterop.FileFlags { UInt32(bitPattern: UF_READONLY) } +internal var _UF_READONLY: UInt32 { UInt32(bitPattern: UF_READONLY) } @_alwaysEmitIntoClient -internal var _UF_REPARSE: CInterop.FileFlags { UInt32(bitPattern: UF_REPARSE) } +internal var _UF_REPARSE: UInt32 { UInt32(bitPattern: UF_REPARSE) } @_alwaysEmitIntoClient -internal var _UF_SPARSE: CInterop.FileFlags { UInt32(bitPattern: UF_SPARSE) } +internal var _UF_SPARSE: UInt32 { UInt32(bitPattern: UF_SPARSE) } @_alwaysEmitIntoClient -internal var _UF_SYSTEM: CInterop.FileFlags { UInt32(bitPattern: UF_SYSTEM) } +internal var _UF_SYSTEM: UInt32 { UInt32(bitPattern: UF_SYSTEM) } @_alwaysEmitIntoClient -internal var _SF_SNAPSHOT: CInterop.FileFlags { UInt32(bitPattern: SF_SNAPSHOT) } +internal var _SF_SNAPSHOT: UInt32 { UInt32(bitPattern: SF_SNAPSHOT) } #endif #endif // !os(Windows) diff --git a/Tests/SystemTests/FileModeTests.swift b/Tests/SystemTests/FileModeTests.swift index ca010335..46c3a5d6 100644 --- a/Tests/SystemTests/FileModeTests.swift +++ b/Tests/SystemTests/FileModeTests.swift @@ -36,6 +36,7 @@ import Android @Suite("FileMode") private struct FileModeTests { + @available(System 99, *) @Test func basics() async throws { var mode = FileMode(rawValue: S_IFREG | 0o644) // Regular file, rw-r--r-- #expect(mode.type == .regular) @@ -66,6 +67,7 @@ private struct FileModeTests { #expect(mode.type == mode2.type) } + @available(System 99, *) @Test func invalidInput() async throws { // No permissions, all other bits set var invalidMode = FileMode(rawValue: ~0o7777) diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift index 4fefe7cf..299dd8ee 100644 --- a/Tests/SystemTests/StatTests.swift +++ b/Tests/SystemTests/StatTests.swift @@ -39,6 +39,7 @@ import Android @Suite("Stat") private struct StatTests { + @available(System 99, *) @Test func basics() async throws { try withTemporaryFilePath(basename: "Stat_basics") { tempDir in let dirStatFromFilePath = try tempDir.stat() @@ -80,8 +81,8 @@ private struct StatTests { } } - @Test - func followSymlinkInits() async throws { + @available(System 99, *) + @Test func followSymlinkInits() async throws { try withTemporaryFilePath(basename: "Stat_followSymlinkInits") { tempDir in let targetFilePath = tempDir.appending("target.txt") let symlinkPath = tempDir.appending("symlink") @@ -189,6 +190,7 @@ private struct StatTests { } } + @available(System 99, *) @Test func permissions() async throws { try withTemporaryFilePath(basename: "Stat_permissions") { tempDir in let testFile = tempDir.appending("test.txt") @@ -218,8 +220,8 @@ private struct StatTests { } } - @Test - func times() async throws { + @available(System 99, *) + @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 @@ -347,6 +349,7 @@ private struct StatTests { } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + @available(System 99, *) @Test func flags() async throws { try withTemporaryFilePath(basename: "Stat_flags") { tempDir in let filePath = tempDir.appending("test.txt") From c2e385e849608d12afdbeeba2d113a74b72eb6bd Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Wed, 29 Oct 2025 12:01:13 -0600 Subject: [PATCH 11/12] Remove commented-out code pt. 2 --- Sources/System/Internals/Constants.swift | 6 ------ Tests/SystemTests/StatTests.swift | 7 ------- 2 files changed, 13 deletions(-) diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 8740d81e..3e71ec90 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -663,12 +663,6 @@ internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } 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 diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift index 299dd8ee..524226d2 100644 --- a/Tests/SystemTests/StatTests.swift +++ b/Tests/SystemTests/StatTests.swift @@ -400,13 +400,6 @@ private struct StatTests { } -// 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) From 87f8e49ba79c68598b36cbe5f04f762bbcd32f74 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Thu, 30 Oct 2025 14:11:45 -0600 Subject: [PATCH 12/12] Assign rawValue instead of self --- Sources/System/FileSystem/Stat.swift | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 98b7dc19..c335cf80 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -102,7 +102,7 @@ public struct Stat: RawRepresentable, Sendable { followTargetSymlink: Bool = true, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try path.withPlatformString { + self.rawValue = try path.withPlatformString { Self._stat( $0, followTargetSymlink: followTargetSymlink, @@ -125,7 +125,7 @@ public struct Stat: RawRepresentable, Sendable { followTargetSymlink: Bool = true, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try Self._stat( + self.rawValue = try Self._stat( path, followTargetSymlink: followTargetSymlink, retryOnInterrupt: retryOnInterrupt @@ -137,7 +137,7 @@ public struct Stat: RawRepresentable, Sendable { _ ptr: UnsafePointer, followTargetSymlink: Bool, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { if followTargetSymlink { @@ -145,7 +145,7 @@ public struct Stat: RawRepresentable, Sendable { } else { system_lstat(ptr, &result) } - }.map { Stat(rawValue: result) } + }.map { result } } /// Creates a `Stat` struct from a `FileDescriptor`. @@ -156,7 +156,7 @@ public struct Stat: RawRepresentable, Sendable { _ fd: FileDescriptor, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try Self._fstat( + self.rawValue = try Self._fstat( fd, retryOnInterrupt: retryOnInterrupt ).get() @@ -166,11 +166,11 @@ public struct Stat: RawRepresentable, Sendable { internal static func _fstat( _ fd: FileDescriptor, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { system_fstat(fd.rawValue, &result) - }.map { Stat(rawValue: result) } + }.map { result } } /// Creates a `Stat` struct from a `FilePath` and `Flags`. @@ -184,7 +184,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try path.withPlatformString { + self.rawValue = try path.withPlatformString { Self._fstatat( $0, relativeTo: _AT_FDCWD, @@ -208,7 +208,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try path.withPlatformString { + self.rawValue = try path.withPlatformString { Self._fstatat( $0, relativeTo: fd.rawValue, @@ -229,7 +229,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try Self._fstatat( + self.rawValue = try Self._fstatat( path, relativeTo: _AT_FDCWD, flags: flags, @@ -251,7 +251,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try Self._fstatat( + self.rawValue = try Self._fstatat( path, relativeTo: fd.rawValue, flags: flags, @@ -265,11 +265,11 @@ public struct Stat: RawRepresentable, Sendable { relativeTo fd: FileDescriptor.RawValue, flags: Stat.Flags, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { system_fstatat(fd, path, &result, flags.rawValue) - }.map { Stat(rawValue: result) } + }.map { result } }