From 0610d8f893d5eec935883802bb6f9eb31c2b845b Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 12 Aug 2025 11:44:05 -0600 Subject: [PATCH 1/4] Stat proposal --- NNNN-system-stat.md | 890 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 890 insertions(+) create mode 100644 NNNN-system-stat.md diff --git a/NNNN-system-stat.md b/NNNN-system-stat.md new file mode 100644 index 00000000..a44aa422 --- /dev/null +++ b/NNNN-system-stat.md @@ -0,0 +1,890 @@ +# Stat for Swift System + +* Proposal: [SE-NNNN](NNNN-system-stat.md) +* Authors: [Jonathan Flat](https://github.com/jrflat), [Michael Ilseman](https://github.com/milseman), [Rauhul Varma](https://github.com/rauhul) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: [apple/swift-system#256](https://github.com/apple/swift-system/pull/256) +* Review: ([pitch](https://forums.swift.org/)) + +## Introduction + +This proposal introduces a Swift-native `Stat` type to the System library, providing comprehensive access to file metadata on Unix-like platforms through type-safe, platform-aware APIs that wrap the C `stat` types and system calls. + +## Motivation + +Currently, Swift developers who want to work with the file system's lowest level API can only do so through bridged C interfaces. These interfaces lack type safety and require writing non-idiomatic Swift, leading to errors and confusion. + +The goal of the `Stat` type is to provide a faithful and performant Swift wrapper around the underlying C system calls while adding type safety, platform abstraction, and improved discoverability/usability with clear naming. For more on the motivation behind System, see [https://www.swift.org/blog/swift-system](https://www.swift.org/blog/swift-system) + +## Proposed solution + +This proposal adds a `struct Stat` that is available on Unix-like platforms. See discussion on Windows-specific API in **Future Directions**. + +### `Stat` - File Metadata +A Swift wrapper around the C `stat` struct that provides type-safe access to file metadata: + +```swift +// Get file status from path String +let stat = try Stat("/path/to/file") + +// From FileDescriptor +let stat = try fd.stat() + +// From FilePath +let stat = try filePath.stat() + +// `followTargetSymlink: true` (default) behaves like `stat()` +// `followTargetSymlink: false` behaves like `lstat()` +let stat = try symlinkPath.stat(followTargetSymlink: false) + +// Supply flags and optional file descriptor to use the `fstatat()` variant +let stat = try Stat("path/to/file", relativeTo: fd, flags: .symlinkNoFollow) + +print("Size: \(stat.size) bytes") +print("Type: \(stat.type)") // .regular, .directory, .symbolicLink, etc. +print("Permissions: \(stat.permissions)") +print("Modified: \(stat.modificationTime)") + +// Platform-specific information when available +#if canImport(Darwin) || os(FreeBSD) +print("Creation time: \(stat.creationTime)") +#endif +``` + +### Error Handling + +All initializers throw the existing `Errno` type: + +```swift +do { + let stat = try Stat("/nonexistent/file") +} catch Errno.noSuchFileOrDirectory { + print("File not found") +} catch { + print("Other error: \(error)") +} +``` + +These initializers use a typed `throws(Errno)` and require Swift 6.0 or later. + +## Detailed design + +See the **Appendix** section at the end of this proposal for a table view of Swift API to C mappings. + +All API are marked `@_alwaysEmitIntoClient` for performance and back-dating of availability. + +### FileType + +This proposal introduces `FileType` and `FileMode` types to represent `mode_t` values from the C `stat` struct. The type and permissions of a `FileMode` can be modified for convenience, and `FileMode` handles the respective bit masking. + +```swift +/// A file type matching those contained in a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +public struct FileType: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw file-type bits from the C mode. + 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`. + public init(rawValue: CInterop.Mode) + + /// Directory + /// + /// The corresponding C constant is `S_IFDIR`. + public static var directory: FileType { get } + + /// Character special device + /// + /// The corresponding C constant is `S_IFCHR`. + public static var characterSpecial: FileType { get } + + /// Block special device + /// + /// The corresponding C constant is `S_IFBLK`. + public static var blockSpecial: FileType { get } + + /// Regular file + /// + /// The corresponding C constant is `S_IFREG`. + public static var regular: FileType { get } + + /// FIFO (or pipe) + /// + /// The corresponding C constant is `S_IFIFO`. + public static var pipe: FileType { get } + + /// Symbolic link + /// + /// The corresponding C constant is `S_IFLNK`. + public static var symbolicLink: FileType { get } + + /// Socket + /// + /// The corresponding C constant is `S_IFSOCK`. + public static var socket: FileType { get } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Whiteout file + /// + /// The corresponding C constant is `S_IFWHT`. + public static var whiteout: FileType { get } + #endif +} +``` + +### FileMode +```swift +/// A strongly-typed file mode representing a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C mode. + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed `FileMode` from the raw C value. + public init(rawValue: CInterop.Mode) + + /// Creates a `FileMode` from the given file type and permissions. + /// + /// - Note: This initializer masks the inputs with their respective bit masks. + public init(type: FileType, permissions: FilePermissions) + + /// 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`. + public var type: FileType { get set } + + /// The file's permissions, from the mode's permission bits. + /// + /// Setting this property will mask the `newValue` with the permissions bit mask `0o7777`. + public var permissions: FilePermissions { get set } +} +``` + +### Supporting ID Types + +This proposal also uses new `DeviceID`, `UserID`, `GroupID`, and `Inode` types to represent the respective C data types found in `stat`. These are strongly-typed structs instead of `CInterop` typealiases to prevent ambiguity in future System implementations and to allow for added functionality. + +For example, with an implementation of `chown`, a developer might accidentally misplace user and group parameters with no warning if both were a typealias of the underlying `unsigned int`. Furthermore, a strongly-typed `DeviceID` would allow us to add functionality such as a `makedev` function, or `major` and `minor` getters. + +For now, we define the following for use in `Stat`. + +```swift +@frozen +public struct UserID: RawRepresentable, Sendable, Hashable, Codable { + public var rawValue: CInterop.UserID + public init(rawValue: CInterop.UserID) +} + +@frozen +public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { + public var rawValue: CInterop.GroupID + public init(rawValue: CInterop.GroupID) +} + +@frozen +public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { + public var rawValue: CInterop.DeviceID + public init(rawValue: CInterop.DeviceID) +} + +@frozen +public struct Inode: RawRepresentable, Sendable, Hashable, Codable { + public var rawValue: CInterop.Inode + public init(rawValue: CInterop.Inode) +} +``` + +Each type stores a `CInterop` typealias to ensure an appropriate `rawValue` for the current platform. Added functionality is outside the scope of this proposal and will be included in a future proposal. + +### FileFlags + +A new `FileFlags` type represents file-specific flags found in a `stat` struct on Darwin, FreeBSD, and OpenBSD. This type would also be useful for an implementation of `chflags()`. + +```swift +/// 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 +public struct FileFlags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + public let rawValue: CInterop.FileFlags + + /// Creates a strongly-typed `FileFlags` from the raw C value. + public init(rawValue: CInterop.FileFlags) + + // 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. + public static var noDump: FileFlags { get } + + /// File may not be changed. + /// + /// The corresponding C constant is `UF_IMMUTABLE`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var userImmutable: FileFlags { get } + + /// 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. + public static var userAppend: FileFlags { get } + + /// File has been archived. + /// + /// The corresponding C constant is `SF_ARCHIVED`. + /// - Note: This flag may only be changed by the superuser. + public static var archived: FileFlags { get } + + /// File may not be changed. + /// + /// The corresponding C constant is `SF_IMMUTABLE`. + /// - Note: This flag may only be changed by the superuser. + public static var systemImmutable: FileFlags { get } + + /// Writes to the file may only append. + /// + /// The corresponding C constant is `SF_APPEND`. + /// - Note: This flag may only be changed by the superuser. + public static var systemAppend: FileFlags { get } + + // 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. + public static var opaque: FileFlags { get } + + /// 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. + public static var compressed: FileFlags { get } + + /// 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. + public static var tracked: FileFlags { get } + + /// 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. + public static var hidden: FileFlags { get } + + /// File requires an entitlement for writing. + /// + /// The corresponding C constant is `SF_RESTRICTED`. + /// - Note: This flag may only be changed by the superuser. + public static var restricted: FileFlags { get } + + /// File may not be removed or renamed. + /// + /// The corresponding C constant is `SF_NOUNLINK`. + /// - Note: This flag may only be changed by the superuser. + public static var systemNoUnlink: FileFlags { get } + #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. + public static var dataVault: FileFlags { get } + + /// 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. + public static var firmlink: FileFlags { get } + + /// 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. + public static var dataless: FileFlags { get } + #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. + public static var userNoUnlink: FileFlags { get } + + /// 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. + public static var offline: FileFlags { get } + + /// 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. + public static var readOnly: FileFlags { get } + + /// 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. + public static var reparse: FileFlags { get } + + /// 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. + public static var sparse: FileFlags { get } + + /// 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. + public static var system: FileFlags { get } + + /// File is a snapshot. + /// + /// The corresponding C constant is `SF_SNAPSHOT`. + /// - Note: This flag may only be changed by the superuser. + public static var snapshot: FileFlags { get } + #endif +} +``` + +### Stat + +`Stat` can be initialized from a `FilePath`, `UnsafePointer`, or `FileDescriptor`. This proposal also includes functions on `FileDescriptor` and `FilePath` for creating a `Stat` object, seen in the section below. + +The initializer accepting a `FileDescriptor` corresponds to `fstat()`. If the file descriptor points to a symlink, this will return information about the symlink itself. + +In the non-`FileDescriptor` case, one form of the initializer takes a `followTargetSymlink: Bool = true` parameter. The default `true` corresponds to `stat()` and will follow a symlink at the end of the path. Setting `followTargetSymlink: false` corresponds to `lstat()` and will return information about the symlink itself. + +The other form of the initializer receives a path, which can be optionally resolved against a given file descriptor, and a set of `Stat.Flags`. These APIs correspond to the `fstatat()` system call and use a default file descriptor of `AT_FDCWD` if one isn't supplied. + +```swift +/// A Swift wrapper of the C `stat` struct. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +public struct Stat: RawRepresentable, Sendable { + + /// The raw C `stat` struct. + public var rawValue: CInterop.Stat + + /// Creates a Swift `Stat` from the raw C struct. + public init(rawValue: CInterop.Stat) + + // MARK: Stat.Flags + + /// Flags representing those passed to `fstatat()`. + @frozen + public struct Flags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + public let rawValue: CInt + + /// Creates a strongly-typed `Stat.Flags` from raw C flags. + public init(rawValue: CInt) + + /// If the path ends with a symbolic link, return information about the link itself. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW`. + public static var symlinkNoFollow: Flags { get } + + #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. + public static var symlinkNoFollowAny: Flags { get } + #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. + public static var resolveBeneath: Flags { get } + #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. + public static var emptyPath: Flags { get } + #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. + public init( + _ path: FilePath, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// 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. + public init( + _ path: UnsafePointer, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// Creates a `Stat` struct from a `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + public init( + _ fd: FileDescriptor, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// 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()`. + public init( + _ path: FilePath, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// 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()`. + public init( + _ path: FilePath, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// 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()`. + public init( + _ path: UnsafePointer, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// 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()`. + public init( + _ path: UnsafePointer, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) + + // MARK: Properties + + /// ID of device containing file + /// + /// The corresponding C property is `st_dev`. + public var deviceID: DeviceID { get set } + + /// Inode number + /// + /// The corresponding C property is `st_ino`. + public var inode: Inode { get set } + + /// File mode + /// + /// The corresponding C property is `st_mode`. + public var mode: FileMode { get set } + + /// File type for the given mode + public var type: FileType { get set } + + /// File permissions for the given mode + public var permissions: FilePermissions { get set } + + /// Number of hard links + /// + /// The corresponding C property is `st_nlink`. + public var linkCount: Int { get set } + + /// User ID of owner + /// + /// The corresponding C property is `st_uid`. + public var userID: UserID { get set } + + /// Group ID of owner + /// + /// The corresponding C property is `st_gid`. + public var groupID: GroupID { get set } + + /// 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`. + public var specialDeviceID: DeviceID { get set } + + /// Total size, in bytes + /// + /// The corresponding C property is `st_size`. + public var size: Int64 { get set } + + /// Block size for filesystem I/O, in bytes + /// + /// The corresponding C property is `st_blksize`. + public var preferredIOBlockSize: Int { get set } + + /// Number of 512-byte blocks allocated + /// + /// The corresponding C property is `st_blocks`. + public var blocksAllocated: Int64 { get set } + + /// Total size allocated, in bytes + /// + /// - Note: Calculated as `512 * blocksAllocated`. + public var sizeAllocated: Int64 { get } + + /// Time of last access, given as a `UTCClock.Instant` + /// + /// 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: UTCClock.Instant { get set } + + /// Time of last modification, given as a `UTCClock.Instant` + /// + /// 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: UTCClock.Instant { get set } + + /// Time of last status (inode) change, given as a `UTCClock.Instant` + /// + /// 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: UTCClock.Instant { get set } + + #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. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var creationTime: UTCClock.Instant { get set } + #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. + public var flags: FileFlags { get set } + + /// 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. + public var generationNumber: Int { get set } + #endif +} + +// MARK: - Equatable and Hashable + +extension Stat: Equatable { + /// Compares the raw bytes of two `Stat` structs for equality. + public static func == (lhs: Self, rhs: Self) -> Bool +} + +extension Stat: Hashable { + /// Hashes the raw bytes of this `Stat` struct. + public func hash(into hasher: inout Hasher) +} +``` + +### FileDescriptor and FilePath Extensions + +```swift +extension FileDescriptor { + + /// Creates a `Stat` struct for the file referenced by this `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + public func stat( + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat +} + +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. + public func stat( + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat + + /// 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()`. + public func stat( + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat + + /// 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()`. + public func stat( + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat +} +``` + +### CInterop Extensions + +This proposal extends the existing `CInterop` namespace with platform-appropriate typealiases for the underlying C types. These typealiases are used as the `rawValue` for their strongly-typed representations. + +```swift +extension CInterop { + public typealias Stat + public typealias Inode + public typealias UserID + public typealias GroupID + public typealias DeviceID + public typealias FileFlags +} +``` + +## Source compatibility + +This proposal is additive and source-compatible with existing code. + +## ABI compatibility + +This proposal is additive and ABI-compatible with existing code. + +## Implications on adoption + +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility. + +## Future directions + +To remain faithful to the underlying system calls, we don't anticipate extending `Stat`. However, the types introduced in this proposal could serve as the foundation of broader file system APIs in Swift. + +While this proposal does not include `Stat` on Windows, a separate proposal should provide Swift-native wrappers of idiomatic `GetFileInformation` functions with their associated types. + +A more general `FileInfo` API could then build on these OS-specific types to provide an ergonomic, cross-platform abstraction for file metadata. These future cross-platform APIs might be better implemented outside of System, such as in Foundation, the standard library, or somewhere in between. They could provide additional information or conveniences, such as reading and modifying extended attributes or setting file timestamps. + +In the future, more functionality could be added to types such as `DeviceID`. + +## Alternatives considered + +### `FileInfo` as the lowest-level type +An alternative approach could be to have a more general `FileInfo` type be the lowest level of abstraction provided by the System library. This type would then handle all the `stat` or Windows-specific struct storage and accessors. However, this alternative: + +- Is inconsistent with System's philosophy of providing low-level system abstractions. +- Introduces an even larger number of system-specific APIs on each type. +- Misses out on the familiarity of the `stat` name. Developers know what to look for and what to expect from this type. + +### Single combined type for both file and file system metadata +Combining `Stat` and `StatFS` (separate proposal) into a single type was considered but rejected because file and file system information serve different purposes and are typically needed in different contexts. Storing and/or initializing both `stat` and `statfs` structs unnecessarily reduces performance when one isn't needed. + +### Making `Stat` available on Windows +It's possible to make `Stat` available on Windows and use either the non-native `_stat` functions from CRT or populate the information via a separate `GetFileInformation` call. However, many of the `stat` fields are not- or less-applicable on Windows and are treated as such by `_stat`. For instance, `st_uid` is always zero on Windows, `st_ino` has no meaning in FAT, HPFS, or NTFS file systems, and `st_mode` can only specify a regular file, directory, or character special, with the executable bit depending entirely on the file's path extension. + +Rather than forcing Windows file metadata semantics into a cross-platform `Stat` type, we should instead create Windows-specific types that give developers full access to platform-native file metadata. Combined with a higher-level `FileInfo` type that _is_ cross-platform, this gives the best of both low-level and platform-agnostic APIs. + +### Only have `FilePath` and `FileDescriptor` extensions rather than initializers that accept these types +While having `.stat()` functions on `FilePath` and `FileDescriptor` is preferred for ergonomics and function chaining, this technique might lack the discoverability of having an initializer on `Stat` directly. This proposal therefore includes both the initializers and extensions. + +### Types for time properties + +`UTCClock.Instant` was chosen over alternatives such as `Duration` or a new `Timespec` type to provide a comparable instant in time rather than a duration since the Epoch. This would depend on lowering `UTCClock` to System or the standard library, which could be discussed in a separate pitch or proposal. + +### Type names + +`Stat` was chosen over alternatives like `FileStat` or `FileStatus` for its brevity and likeness to the "stat" system call. Unlike generic names such as `FileInfo` or `FileMetadata`, `Stat` emphasizes the platform-specific nature of this type. + +`Inode` was similarly chosen over alternatives like `FileIndex` or `FileID` to emphasize the platform-specific nature. `IndexNode` is a bit verbose, and despite its etymology, "inode" is now ubiquitous and understood as a single word, making the capitalization `Inode` preferable to `INode`. + + +## Acknowledgments + +These new APIs build on excellent types currently available in the System library. + +## Appendix + +### Swift API to C Mappings + +The following tables show the mapping between Swift APIs and their underlying C system calls across different operating systems: + +#### `Stat` Initializer Mappings + +The `retryOnInterrupt: Bool = true` parameter is omitted for clarity. + +| Swift API | Unix-like Platforms | +|-----------|---------------------| +| `Stat(_ path: FilePath, followTargetSymlink: true)` | `stat()` | +| `Stat(_ path: UnsafePointer, followTargetSymlink: true)` | `stat()` | +|| +| `Stat(_ path: FilePath, followTargetSymlink: false)` | `lstat()` | +| `Stat(_ path: UnsafePointer, followTargetSymlink: false)` | `lstat()` | +|| +| `Stat(_ path: FilePath, relativeTo: FileDescriptor, flags: Stat.Flags)` | `fstatat()` | +| `Stat(_ path: UnsafePointer, relativeTo: FileDescriptor, flags: Stat.Flags)` | `fstatat()` | +|| +| `Stat(_ fd: FileDescriptor)` | `fstat()` | +| `FileDescriptor.stat()` | `fstat()` | +|| +| `FilePath.stat(followTargetSymlink: true)` | `stat()` | +| `FilePath.stat(followTargetSymlink: false)` | `lstat()` | +| `FilePath.stat(relativeTo: FileDescriptor, flags: Stat.Flags)` | `fstatat()` | + +#### `Stat` Property Mappings + +`"` denotes the same property name across all operating systems. + +| Swift Property | Darwin | FreeBSD | OpenBSD | Linux | Android | WASI | +|----------------|--------|---------|---------|-------|---------|------| +| `deviceID` | `st_dev` | " | " | " | " | " | +| `inode` | `st_ino` | " | " | " | " | " | +| `mode` | `st_mode` | " | " | " | " | " | +| `linkCount` | `st_nlink` | " | " | " | " | " | +| `userID` | `st_uid` | " | " | " | " | " | +| `groupID` | `st_gid` | " | " | " | " | " | +| `specialDeviceID` | `st_rdev` | " | " | " | " | " | +| `size` | `st_size` | " | " | " | " | " | +| `preferredIOBlockSize` | `st_blksize` | " | " | " | " | " | +| `blocksAllocated` | `st_blocks` | " | " | " | " | " | +| `accessTime` | `st_atimespec` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | +| `modificationTime` | `st_mtimespec` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | +| `changeTime` | `st_ctimespec` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | +| `creationTime` | `st_birthtimespec` | `st_birthtim` | N/A | N/A | N/A | N/A | +| `flags` | `st_flags` | `st_flags` | `st_flags` | N/A | N/A | N/A | +| `generationNumber` | `st_gen` | `st_gen` | `st_gen` | N/A | N/A | N/A | + +#### `Stat.Flags` Mappings + +| Swift Flag | Darwin | FreeBSD | OpenBSD | Linux | Android | WASI | +|------------|--------|---------|---------|-------|---------|------| +| `symlinkNoFollow` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | +| `symlinkNoFollowAny` | `AT_SYMLINK_NOFOLLOW_ANY` | N/A | N/A | N/A | N/A | N/A | +| `resolveBeneath` | `AT_RESOLVE_BENEATH` | `AT_RESOLVE_BENEATH` | N/A | N/A | N/A | N/A | +| `emptyPath` | N/A | `AT_EMPTY_PATH` | N/A | `AT_EMPTY_PATH` | `AT_EMPTY_PATH` | N/A | + +#### `FileFlags` Mappings + +**Note:** `FileFlags` is only available on Darwin, FreeBSD, and OpenBSD. + +| Swift Flag | 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 | From 0b3b691d0b292bc0037f79db0a2bb858b7d5d942 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 12 Aug 2025 12:36:17 -0600 Subject: [PATCH 2/4] Update pitch link --- NNNN-system-stat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NNNN-system-stat.md b/NNNN-system-stat.md index a44aa422..adee11f1 100644 --- a/NNNN-system-stat.md +++ b/NNNN-system-stat.md @@ -5,7 +5,7 @@ * Review Manager: TBD * Status: **Awaiting review** * Implementation: [apple/swift-system#256](https://github.com/apple/swift-system/pull/256) -* Review: ([pitch](https://forums.swift.org/)) +* Review: ([pitch](https://forums.swift.org/t/pitch-stat-types-for-swift-system/81616)) ## Introduction From 70f197f03691659bf3e4086056a3ac034a43401f Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 29 Sep 2025 11:19:36 -0600 Subject: [PATCH 3/4] Address feedback (v3) --- NNNN-system-stat.md | 202 ++++++++++++++++++++++++++++++++------------ 1 file changed, 147 insertions(+), 55 deletions(-) diff --git a/NNNN-system-stat.md b/NNNN-system-stat.md index adee11f1..f7d8f2cc 100644 --- a/NNNN-system-stat.md +++ b/NNNN-system-stat.md @@ -1,12 +1,18 @@ # Stat for Swift System -* Proposal: [SE-NNNN](NNNN-system-stat.md) +* Proposal: [SYS-0006](NNNN-system-stat.md) * Authors: [Jonathan Flat](https://github.com/jrflat), [Michael Ilseman](https://github.com/milseman), [Rauhul Varma](https://github.com/rauhul) * Review Manager: TBD * Status: **Awaiting review** * Implementation: [apple/swift-system#256](https://github.com/apple/swift-system/pull/256) * Review: ([pitch](https://forums.swift.org/t/pitch-stat-types-for-swift-system/81616)) +#### Revision history + +* **v1** Initial version +* **v2** Moved `UTCClock.Instant` properties to **Future Directions** and exposed C `timespec` properties. Expanded **Alternatives Considered** for `Stat` name and time properties. +* **v3** Add `init(_:)` to wrapper types, clarify `FileType(rawValue:)` behavior with `S_IFMT`, rename `.pipe` to `.fifo`, mention `ALLPERMS` instead of `0o7777`, explain "is"-less flag names in **Alternatives Considered**, fix conditionals for FreeBSD flags, clarify that `.type` and `.permissions` depend on `.mode`, clarify that size property behaviors are file system-dependent. + ## Introduction This proposal introduces a Swift-native `Stat` type to the System library, providing comprehensive access to file metadata on Unix-like platforms through type-safe, platform-aware APIs that wrap the C `stat` types and system calls. @@ -42,6 +48,7 @@ let stat = try symlinkPath.stat(followTargetSymlink: false) let stat = try Stat("path/to/file", relativeTo: fd, flags: .symlinkNoFollow) print("Size: \(stat.size) bytes") +print("Size allocated: \(stat.sizeAllocated) bytes") print("Type: \(stat.type)") // .regular, .directory, .symbolicLink, etc. print("Permissions: \(stat.permissions)") print("Modified: \(stat.modificationTime)") @@ -88,11 +95,17 @@ public struct FileType: RawRepresentable, Sendable, Hashable, Codable { /// The raw file-type bits from the C mode. 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`. + /// Creates a strongly-typed file type from the raw C `mode_t`. + /// + /// - Note: This initializer stores the `rawValue` directly and **does not** + /// mask the value with `S_IFMT`. If the supplied `rawValue` contains bits + /// outside of the `S_IFMT` mask, the resulting `FileType` will not compare + /// equal to constants like `.directory` and `.symbolicLink`, which may + /// be unexpected. + /// + /// If you're unsure whether the `mode_t` contains bits outside of `S_IFMT`, + /// you can use `FileMode(rawValue:)` instead to get a strongly-typed + /// `FileMode`, then call `.type` to get the properly masked `FileType`. public init(rawValue: CInterop.Mode) /// Directory @@ -115,10 +128,10 @@ public struct FileType: RawRepresentable, Sendable, Hashable, Codable { /// The corresponding C constant is `S_IFREG`. public static var regular: FileType { get } - /// FIFO (or pipe) + /// FIFO (or named pipe) /// /// The corresponding C constant is `S_IFIFO`. - public static var pipe: FileType { get } + public static var fifo: FileType { get } /// Symbolic link /// @@ -152,6 +165,9 @@ public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { /// Creates a strongly-typed `FileMode` from the raw C value. public init(rawValue: CInterop.Mode) + + /// Creates a strongly-typed `FileMode` from the raw C value. + public init(_ rawValue: CInterop.Mode) /// Creates a `FileMode` from the given file type and permissions. /// @@ -165,7 +181,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`. public var permissions: FilePermissions { get set } } ``` @@ -183,24 +199,28 @@ For now, we define the following for use in `Stat`. public struct UserID: RawRepresentable, Sendable, Hashable, Codable { public var rawValue: CInterop.UserID public init(rawValue: CInterop.UserID) + public init(_ rawValue: CInterop.UserID) } @frozen public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { public var rawValue: CInterop.GroupID public init(rawValue: CInterop.GroupID) + public init(_ rawValue: CInterop.GroupID) } @frozen public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { public var rawValue: CInterop.DeviceID public init(rawValue: CInterop.DeviceID) + public init(_ rawValue: CInterop.DeviceID) } @frozen public struct Inode: RawRepresentable, Sendable, Hashable, Codable { public var rawValue: CInterop.Inode public init(rawValue: CInterop.Inode) + public init(_ rawValue: CInterop.Inode) } ``` @@ -271,30 +291,12 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { /// - Note: This flag may be changed by the file owner or superuser. public static var opaque: FileFlags { get } - /// 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. - public static var compressed: FileFlags { get } - - /// 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. - public static var tracked: FileFlags { get } - /// 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. public static var hidden: FileFlags { get } - /// File requires an entitlement for writing. - /// - /// The corresponding C constant is `SF_RESTRICTED`. - /// - Note: This flag may only be changed by the superuser. - public static var restricted: FileFlags { get } - /// File may not be removed or renamed. /// /// The corresponding C constant is `SF_NOUNLINK`. @@ -305,11 +307,29 @@ 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. + public static var compressed: FileFlags { get } + + /// 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. + public static var tracked: FileFlags { get } + /// 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. public static var dataVault: FileFlags { get } + + /// File requires an entitlement for writing. + /// + /// The corresponding C constant is `SF_RESTRICTED`. + /// - Note: This flag may only be changed by the superuser. + public static var restricted: FileFlags { get } /// File is a firmlink. /// @@ -449,6 +469,7 @@ public struct Stat: RawRepresentable, Sendable { /// /// The corresponding C constant is `AT_RESOLVE_BENEATH`. /// - Note: Only available on Darwin and FreeBSD. + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) public static var resolveBeneath: Flags { get } #endif @@ -478,7 +499,7 @@ public struct Stat: RawRepresentable, Sendable { retryOnInterrupt: Bool = true ) throws(Errno) - /// 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()`. @@ -568,9 +589,15 @@ public struct Stat: RawRepresentable, Sendable { public var mode: FileMode { get set } /// File type for the given mode + /// + /// - Note: This property is equivalent to `mode.type`. Modifying this + /// property will update the underlying `st_mode` accordingly. public var type: FileType { get set } /// File permissions for the given mode + /// + /// - Note: This property is equivalent to `mode.permissions`. Modifying + /// this property will update the underlying `st_mode` accordingly. public var permissions: FilePermissions { get set } /// Number of hard links @@ -599,6 +626,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`. public var size: Int64 { get set } @@ -609,39 +642,46 @@ 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`. public var blocksAllocated: Int64 { get set } /// 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`. public var sizeAllocated: Int64 { get } - - /// Time of last access, given as a `UTCClock.Instant` + + // 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. + // See Future Directions for more info. + + /// 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: UTCClock.Instant { get set } - - /// Time of last modification, given as a `UTCClock.Instant` + public var st_atim: timespec { get set } + + /// 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: UTCClock.Instant { get set } - - /// Time of last status (inode) change, given as a `UTCClock.Instant` + public var st_mtim: timespec { get set } + + /// 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: UTCClock.Instant { get set } + public var st_ctim: timespec { get set } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - /// Time of file creation, given as a `UTCClock.Instant` + /// 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: UTCClock.Instant { get set } + public var st_birthtim: timespec { get set } #endif #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) @@ -703,7 +743,7 @@ extension FilePath { retryOnInterrupt: Bool = true ) throws(Errno) -> Stat - /// 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. /// @@ -713,7 +753,7 @@ extension FilePath { retryOnInterrupt: Bool = true ) throws(Errno) -> Stat - /// 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. @@ -761,10 +801,43 @@ To remain faithful to the underlying system calls, we don't anticipate extending While this proposal does not include `Stat` on Windows, a separate proposal should provide Swift-native wrappers of idiomatic `GetFileInformation` functions with their associated types. -A more general `FileInfo` API could then build on these OS-specific types to provide an ergonomic, cross-platform abstraction for file metadata. These future cross-platform APIs might be better implemented outside of System, such as in Foundation, the standard library, or somewhere in between. They could provide additional information or conveniences, such as reading and modifying extended attributes or setting file timestamps. +A more general `FileInfo` API could then build on these OS-specific types to provide an ergonomic, cross-platform abstraction for file metadata. These future cross-platform APIs might be better implemented outside of System, such as in Foundation, the standard library, or somewhere in between. They could provide additional information or convenience features, such as reading and modifying extended attributes or setting file timestamps. In the future, more functionality could be added to types such as `DeviceID`. +### Using `UTCClock.Instant` for time properties + +When the `UTCClock` proposal and code destination is finalized, we could use the `UTCClock.Instant` type for `Stat` time properties: + +```swift +extension Stat { + /// 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 set } + + /// 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 set } + + /// 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 set } + + #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 set } + #endif +} +``` + +We would reserve the more ergonomic `accessTime`, `modificationTime`, etc. names for these future extensions. + ## Alternatives considered ### `FileInfo` as the lowest-level type @@ -787,14 +860,33 @@ While having `.stat()` functions on `FilePath` and `FileDescriptor` is preferred ### Types for time properties -`UTCClock.Instant` was chosen over alternatives such as `Duration` or a new `Timespec` type to provide a comparable instant in time rather than a duration since the Epoch. This would depend on lowering `UTCClock` to System or the standard library, which could be discussed in a separate pitch or proposal. +`UTCClock.Instant` would ideally be chosen over alternatives such as `Duration` or a new `Timespec` type to provide a comparable instant in time rather than a duration since the Epoch. However, this would depend on lowering `UTCClock` to System or the standard library and depends on that separate proposal. + +Exposing a `timespec` property directly also has benefits; it's faithful to the underlying system's type and already has conversion support to/from `Duration` in the standard library. + +Given that `timespec` is not particularly crufty and already has public API supporting its conversions, we decided to expose the raw `timespec` for now under the original C property names (`st_atim`, `st_mtim`, etc.) and reserve more ergonomic names for future extensions. ### Type names `Stat` was chosen over alternatives like `FileStat` or `FileStatus` for its brevity and likeness to the "stat" system call. Unlike generic names such as `FileInfo` or `FileMetadata`, `Stat` emphasizes the platform-specific nature of this type. -`Inode` was similarly chosen over alternatives like `FileIndex` or `FileID` to emphasize the platform-specific nature. `IndexNode` is a bit verbose, and despite its etymology, "inode" is now ubiquitous and understood as a single word, making the capitalization `Inode` preferable to `INode`. +`Stat` and (possible future) `StatFS` names were chosen over `FileStat` and `FileSystemStat`, or a namespaced `File.Stat` and `FileSystem.Stat`, because `Stat` is recognized more as its own concept rather than shorthand for "status." Thus, using `FileSystem.Stat` or `FileSystemStat` for `statfs` in the future might lead to confusion. Also, precedence from other languages' `stat` APIs that use a "file system" namespace might add to this confusion: + +``` +Rust: fs::metadata() -> fs::Metadata +Python: os.stat() -> os.stat_result +Go: os.Stat() -> fs.FileInfo +``` + +`Inode` was chosen over alternatives like `FileIndex` or `FileID` to emphasize the platform-specific nature. `IndexNode` is a bit verbose, and despite its etymology, "inode" is now ubiquitous and understood as a single word, making the capitalization `Inode` preferable to `INode`. + +### `FileFlags` naming conventions + +`FileFlags` property names such as `hidden` and `compressed` could alternatively use an "is" prefix commonly seen in boolean properties to form `.isHidden` and `.isCompressed`. However, we chose to omit the "is" prefix for the following reasons: +- The "is"-less flag names are succinct and closely aligned with the underlying C constants they represent. +- `OptionSet` property names often use an adjective ("hidden") rather than a predicate ("is hidden") when describing a single subject, such as a file. This is likely because "is" does not add to the flow of `flags.contains(.isHidden)` like it does for a direct boolean property, such as `file.isHidden`. +- For both `OptionSet` APIs that describe a single subject and those that describe a collection of elements, there's precedence to omit the "is". Examples of single-subject `OptionSet` APIs include `UIControl.State`, which uses `.highlighted` and `.disabled` rather than `.isHighlighted` and `.isDisabled`, and `FilePermissions`, which uses `.ownerRead` rather than `.isOwnerReadable`. Examples of multi-subject `OptionSet` APIs include `Edge.Set`, which uses `.top` and `.bottom`, and `ShippingOptions` from the `OptionSet` documentation, which uses `.nextDay`, `.priority`, etc. ## Acknowledgments @@ -844,10 +936,10 @@ The `retryOnInterrupt: Bool = true` parameter is omitted for clarity. | `size` | `st_size` | " | " | " | " | " | | `preferredIOBlockSize` | `st_blksize` | " | " | " | " | " | | `blocksAllocated` | `st_blocks` | " | " | " | " | " | -| `accessTime` | `st_atimespec` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | -| `modificationTime` | `st_mtimespec` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | -| `changeTime` | `st_ctimespec` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | -| `creationTime` | `st_birthtimespec` | `st_birthtim` | N/A | N/A | N/A | N/A | +| `st_atim` | `st_atimespec` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | +| `st_mtim` | `st_mtimespec` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | +| `st_ctim` | `st_ctimespec` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | +| `st_birthtim` | `st_birthtimespec` | `st_birthtim` | N/A | N/A | N/A | N/A | | `flags` | `st_flags` | `st_flags` | `st_flags` | N/A | N/A | N/A | | `generationNumber` | `st_gen` | `st_gen` | `st_gen` | N/A | N/A | N/A | @@ -873,12 +965,12 @@ The `retryOnInterrupt: Bool = true` parameter is omitted for clarity. | `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 | From 2f4c8c216c59c6a1932f7465e81321525cd45e0e Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 29 Sep 2025 11:26:15 -0600 Subject: [PATCH 4/4] Create Proposals directory --- NNNN-system-stat.md => Proposals/0006-system-stat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename NNNN-system-stat.md => Proposals/0006-system-stat.md (99%) diff --git a/NNNN-system-stat.md b/Proposals/0006-system-stat.md similarity index 99% rename from NNNN-system-stat.md rename to Proposals/0006-system-stat.md index f7d8f2cc..39c1bd3a 100644 --- a/NNNN-system-stat.md +++ b/Proposals/0006-system-stat.md @@ -1,6 +1,6 @@ # Stat for Swift System -* Proposal: [SYS-0006](NNNN-system-stat.md) +* Proposal: [SYS-0006](0006-system-stat.md) * Authors: [Jonathan Flat](https://github.com/jrflat), [Michael Ilseman](https://github.com/milseman), [Rauhul Varma](https://github.com/rauhul) * Review Manager: TBD * Status: **Awaiting review**