diff --git a/Sources/CSystem/include/CSystemLinux.h b/Sources/CSystem/include/CSystemLinux.h index 815e142c..5176f208 100644 --- a/Sources/CSystem/include/CSystemLinux.h +++ b/Sources/CSystem/include/CSystemLinux.h @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2020 Apple Inc. and the Swift System project authors + Copyright (c) 2020 - 2026 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 @@ -20,5 +20,25 @@ #include #include #include "io_uring.h" + +// The `ST_*` mount-flag constants in require _GNU_SOURCE. +// Rather than define _GNU_SOURCE module-wide, which clashes with SwiftGlibc, +// expose them through these getters, defined in shims.c under _GNU_SOURCE. +#include +uint64_t _system_get_ST_RDONLY(void); +uint64_t _system_get_ST_NOSUID(void); +uint64_t _system_get_ST_NODEV(void); +uint64_t _system_get_ST_NOEXEC(void); +uint64_t _system_get_ST_SYNCHRONOUS(void); +uint64_t _system_get_ST_MANDLOCK(void); +uint64_t _system_get_ST_NOATIME(void); +uint64_t _system_get_ST_NODIRATIME(void); +uint64_t _system_get_ST_RELATIME(void); +uint64_t _system_get_ST_NOSYMFOLLOW(void); +#if !defined(__ANDROID__) +uint64_t _system_get_ST_WRITE(void); +uint64_t _system_get_ST_APPEND(void); +uint64_t _system_get_ST_IMMUTABLE(void); +#endif #endif diff --git a/Sources/CSystem/include/CSystemWASI.h b/Sources/CSystem/include/CSystemWASI.h index 1c8cd0f2..7ad6d056 100644 --- a/Sources/CSystem/include/CSystemWASI.h +++ b/Sources/CSystem/include/CSystemWASI.h @@ -16,6 +16,10 @@ #include #include // For NAME_MAX +// wasi-libc ships , but the Swift WASILibc module does not +// surface it. Re-export them through CSystem here. +#include + // wasi-libc defines the following constants in a way that Clang Importer can't // understand, so we need to expose them manually. static inline int32_t _getConst_O_ACCMODE(void) { return O_ACCMODE; } diff --git a/Sources/CSystem/shims.c b/Sources/CSystem/shims.c index 46ec0271..9ea06b81 100644 --- a/Sources/CSystem/shims.c +++ b/Sources/CSystem/shims.c @@ -19,7 +19,39 @@ #endif #ifdef __linux__ + +// The `ST_*` mount-flag constants in require _GNU_SOURCE. +// Define it here, in a dedicated translation unit, not in CSystemLinux.h, +// since defining it there changes libc types like `fd_set` for every file +// that imports CSystem and conflicts with SwiftGlibc. #define _GNU_SOURCE +#include +#include + +uint64_t _system_get_ST_RDONLY(void) { return ST_RDONLY; } +uint64_t _system_get_ST_NOSUID(void) { return ST_NOSUID; } +uint64_t _system_get_ST_NODEV(void) { return ST_NODEV; } +uint64_t _system_get_ST_NOEXEC(void) { return ST_NOEXEC; } +uint64_t _system_get_ST_SYNCHRONOUS(void) { return ST_SYNCHRONOUS; } +uint64_t _system_get_ST_MANDLOCK(void) { return ST_MANDLOCK; } +uint64_t _system_get_ST_NOATIME(void) { return ST_NOATIME; } +uint64_t _system_get_ST_NODIRATIME(void) { return ST_NODIRATIME; } +uint64_t _system_get_ST_RELATIME(void) { return ST_RELATIME; } + +// ST_NOSYMFOLLOW was added to glibc's in Linux 5.10. For +// older versions, fall back to the fixed kernel UAPI bit (), +// which is the same value a newer glibc header defines. +#ifndef ST_NOSYMFOLLOW +#define ST_NOSYMFOLLOW 0x2000 +#endif +uint64_t _system_get_ST_NOSYMFOLLOW(void) { return ST_NOSYMFOLLOW; } + +#if !defined(__ANDROID__) +uint64_t _system_get_ST_WRITE(void) { return ST_WRITE; } +uint64_t _system_get_ST_APPEND(void) { return ST_APPEND; } +uint64_t _system_get_ST_IMMUTABLE(void) { return ST_IMMUTABLE; } +#endif + #include #endif diff --git a/Sources/System/CMakeLists.txt b/Sources/System/CMakeLists.txt index a904eb1a..243e2251 100644 --- a/Sources/System/CMakeLists.txt +++ b/Sources/System/CMakeLists.txt @@ -1,7 +1,7 @@ #[[ This source file is part of the Swift System open source project -Copyright (c) 2020 Apple Inc. and the Swift System project authors +Copyright (c) 2020 - 2026 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 @@ -42,7 +42,9 @@ target_sources(SystemPackage PRIVATE FileSystem/FileMode.swift FileSystem/FileType.swift FileSystem/Identifiers.swift - FileSystem/Stat.swift) + FileSystem/MountFlags.swift + FileSystem/Stat.swift + FileSystem/StatFS.swift) if(CMAKE_SYSTEM_NAME STREQUAL Linux) target_sources(SystemPackage PRIVATE IORing/IOCompletion.swift diff --git a/Sources/System/FileSystem/MountFlags.swift b/Sources/System/FileSystem/MountFlags.swift new file mode 100644 index 00000000..622ef617 --- /dev/null +++ b/Sources/System/FileSystem/MountFlags.swift @@ -0,0 +1,522 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2026 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 | +// |-------------------------------------------------------------------------------------------------------------------------------------------| +// | MountFlags | Darwin | FreeBSD | OpenBSD | Linux | Android | WASI | +// |-----------------------------|----------------------|-----------------|-----------------|----------------|----------------|----------------| +// | readOnly | MNT_RDONLY | MNT_RDONLY | MNT_RDONLY | ST_RDONLY | ST_RDONLY | ST_RDONLY | +// | synchronous | MNT_SYNCHRONOUS | MNT_SYNCHRONOUS | MNT_SYNCHRONOUS | ST_SYNCHRONOUS | ST_SYNCHRONOUS | ST_SYNCHRONOUS | +// | noExecution | MNT_NOEXEC | MNT_NOEXEC | MNT_NOEXEC | ST_NOEXEC | ST_NOEXEC | ST_NOEXEC | +// | noSetUserID | MNT_NOSUID | MNT_NOSUID | MNT_NOSUID | ST_NOSUID | ST_NOSUID | ST_NOSUID | +// | noAccessTime | MNT_NOATIME | MNT_NOATIME | MNT_NOATIME | ST_NOATIME | ST_NOATIME | ST_NOATIME | +// | noDevices | MNT_NODEV | N/A | MNT_NODEV | ST_NODEV | ST_NODEV | ST_NODEV | +// | mandatoryLockingPermitted | N/A | N/A | N/A | ST_MANDLOCK | ST_MANDLOCK | ST_MANDLOCK | +// | noDirectoryAccessTime | N/A | N/A | N/A | ST_NODIRATIME | ST_NODIRATIME | ST_NODIRATIME | +// | relativeAccessTime | N/A | N/A | N/A | ST_RELATIME | ST_RELATIME | ST_RELATIME | +// | write | N/A | N/A | N/A | ST_WRITE | N/A | ST_WRITE | +// | appendOnly | N/A | N/A | N/A | ST_APPEND | N/A | ST_APPEND | +// | immutable | N/A | N/A | N/A | ST_IMMUTABLE | N/A | ST_IMMUTABLE | +// | noSymlinkFollow | N/A | MNT_NOSYMFOLLOW | N/A | ST_NOSYMFOLLOW | ST_NOSYMFOLLOW | N/A | +// | asynchronous | MNT_ASYNC | MNT_ASYNC | MNT_ASYNC | N/A | N/A | N/A | +// | exported | MNT_EXPORTED | MNT_EXPORTED | MNT_EXPORTED | N/A | N/A | N/A | +// | local | MNT_LOCAL | MNT_LOCAL | MNT_LOCAL | N/A | N/A | N/A | +// | quota | MNT_QUOTA | MNT_QUOTA | MNT_QUOTA | N/A | N/A | N/A | +// | rootFileSystem | MNT_ROOTFS | MNT_ROOTFS | MNT_ROOTFS | N/A | N/A | N/A | +// | union | MNT_UNION | MNT_UNION | N/A | N/A | N/A | N/A | +// | automounted | MNT_AUTOMOUNTED | MNT_AUTOMOUNTED | N/A | N/A | N/A | N/A | +// | multiLabel | MNT_MULTILABEL | MNT_MULTILABEL | N/A | N/A | N/A | N/A | +// | exportedReadOnly | N/A | MNT_EXRDONLY | MNT_EXRDONLY | N/A | N/A | N/A | +// | exportedByDefault | N/A | MNT_DEFEXPORTED | MNT_DEFEXPORTED | N/A | N/A | N/A | +// | exportedAnonymously | N/A | MNT_EXPORTANON | MNT_EXPORTANON | N/A | N/A | N/A | +// | softUpdates | N/A | MNT_SOFTDEP | MNT_SOFTDEP | N/A | N/A | N/A | +// | contentProtection | MNT_CPROTECT | N/A | N/A | N/A | N/A | N/A | +// | removable | MNT_REMOVABLE | N/A | N/A | N/A | N/A | N/A | +// | quarantine | MNT_QUARANTINE | N/A | N/A | N/A | N/A | N/A | +// | volumeFileSystem | MNT_DOVOLFS | N/A | N/A | N/A | N/A | N/A | +// | noBrowsing | MNT_DONTBROWSE | N/A | N/A | N/A | N/A | N/A | +// | ignoreOwnership | MNT_IGNORE_OWNERSHIP | N/A | N/A | N/A | N/A | N/A | +// | journaled | MNT_JOURNALED | N/A | N/A | N/A | N/A | N/A | +// | noUserExtendedAttributes | MNT_NOUSERXATTR | N/A | N/A | N/A | N/A | N/A | +// | deferWrites | MNT_DEFWRITE | N/A | N/A | N/A | N/A | N/A | +// | noSymlinkFollowAtMountPoint | MNT_NOFOLLOW | N/A | N/A | N/A | N/A | N/A | +// | snapshot | MNT_SNAPSHOT | N/A | N/A | N/A | N/A | N/A | +// | strictAccessTime | MNT_STRICTATIME | N/A | N/A | N/A | N/A | N/A | +// | exportedKerberos | N/A | MNT_EXKERB | N/A | N/A | N/A | N/A | +// | exportedPublic | N/A | MNT_EXPUBLIC | N/A | N/A | N/A | N/A | +// | posixACLs | N/A | MNT_ACLS | N/A | N/A | N/A | N/A | +// | geomJournaled | N/A | MNT_GJOURNAL | N/A | N/A | N/A | N/A | +// | excludedFromDiskFreeReports | N/A | MNT_IGNORE | N/A | N/A | N/A | N/A | +// | nfs4ACLs | N/A | MNT_NFS4ACLS | N/A | N/A | N/A | N/A | +// | noClusterRead | N/A | MNT_NOCLUSTERR | N/A | N/A | N/A | N/A | +// | noClusterWrite | N/A | MNT_NOCLUSTERW | N/A | N/A | N/A | N/A | +// | setUserIDDirectory | N/A | MNT_SUIDDIR | N/A | N/A | N/A | N/A | +// | softUpdateJournaling | N/A | MNT_SUJ | N/A | N/A | N/A | N/A | +// | untrusted | N/A | MNT_UNTRUSTED | N/A | N/A | N/A | N/A | +// | mountedByUser | N/A | MNT_USER | N/A | N/A | N/A | N/A | +// | verified | N/A | MNT_VERIFIED | N/A | N/A | N/A | N/A | +// | noPermissionChecks | N/A | N/A | MNT_NOPERM | N/A | N/A | N/A | +// | writeExecuteAllowed | N/A | N/A | MNT_WXALLOWED | N/A | N/A | N/A | +// |-------------------------------------------------------------------------------------------------------------------------------------------| + +#if !os(Windows) + +/// Options employed when mounting a file system. +/// +/// These are the flags reported in the `f_flags` field of a `statfs` struct on +/// Darwin and BSD, or the `f_flag` field of a `statvfs` struct on other +/// platforms. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +@available(System 199, *) +public struct MountFlags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + @_alwaysEmitIntoClient + public let rawValue: CInterop.MountFlags + + /// Creates a strongly-typed `MountFlags` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.MountFlags) { self.rawValue = rawValue } + + // MARK: Flags Available on All Platforms + + /// The file system is mounted read-only, even for the super-user. + /// + /// The corresponding C constant is `MNT_RDONLY` on Darwin and BSD, + /// or `ST_RDONLY` otherwise. + @_alwaysEmitIntoClient + public static var readOnly: MountFlags { MountFlags(rawValue: _MOUNT_RDONLY) } + + /// The file system is written to synchronously. + /// + /// The corresponding C constant is `MNT_SYNCHRONOUS` on Darwin and BSD, + /// or `ST_SYNCHRONOUS` otherwise. + @_alwaysEmitIntoClient + public static var synchronous: MountFlags { MountFlags(rawValue: _MOUNT_SYNCHRONOUS) } + + /// Programs may not be executed from the file system. + /// + /// The corresponding C constant is `MNT_NOEXEC` on Darwin and BSD, + /// or `ST_NOEXEC` otherwise. + @_alwaysEmitIntoClient + public static var noExecution: MountFlags { MountFlags(rawValue: _MOUNT_NOEXEC) } + + /// Set-user-ID and set-group-ID bits are not honored on the file system. + /// + /// The corresponding C constant is `MNT_NOSUID` on Darwin and BSD, + /// or `ST_NOSUID` otherwise. + @_alwaysEmitIntoClient + public static var noSetUserID: MountFlags { MountFlags(rawValue: _MOUNT_NOSUID) } + + /// Access times are not updated on the file system. + /// + /// The corresponding C constant is `MNT_NOATIME` on Darwin and BSD, + /// or `ST_NOATIME` otherwise. + /// - Note: On OpenBSD, access time may still be updated when the + /// modification or status-change time is also being updated. + @_alwaysEmitIntoClient + public static var noAccessTime: MountFlags { MountFlags(rawValue: _MOUNT_NOATIME) } + + // MARK: Flags Available on All Platforms Except FreeBSD + + #if !os(FreeBSD) + /// Special files may not be interpreted on the file system. + /// + /// The corresponding C constant is `MNT_NODEV` on Darwin and OpenBSD, + /// or `ST_NODEV` otherwise. + /// - Note: Not available on FreeBSD. + @_alwaysEmitIntoClient + public static var noDevices: MountFlags { MountFlags(rawValue: _MOUNT_NODEV) } + #endif + + // MARK: Flags Available on Linux, WASI, and Android + + #if os(Linux) || os(WASI) || os(Android) + /// The file system permits mandatory file locking. + /// + /// The corresponding C constant is `ST_MANDLOCK`. + /// - Note: Only available on Linux, WASI, and Android. + /// - Note: Mandatory locking was deprecated and removed in Linux 5.15. + @_alwaysEmitIntoClient + public static var mandatoryLockingPermitted: MountFlags { MountFlags(rawValue: _ST_MANDLOCK) } + + /// Directory access times are not updated on the file system. + /// + /// The corresponding C constant is `ST_NODIRATIME`. + /// - Note: Only available on Linux, WASI, and Android. + @_alwaysEmitIntoClient + public static var noDirectoryAccessTime: MountFlags { MountFlags(rawValue: _ST_NODIRATIME) } + + /// The access time is only updated if it's earlier than or equal to the file's + /// last modification or status-change time, or if it's more than a day old. + /// + /// The corresponding C constant is `ST_RELATIME`. + /// - Note: Only available on Linux, WASI, and Android. + @_alwaysEmitIntoClient + public static var relativeAccessTime: MountFlags { MountFlags(rawValue: _ST_RELATIME) } + #endif + + // MARK: Flags Available on Linux and WASI Only + + #if os(Linux) || os(WASI) + /// Files, directories, and symbolic links on the file system are writable. + /// + /// The corresponding C constant is `ST_WRITE`. + /// - Note: Only available on Linux and WASI. + @_alwaysEmitIntoClient + public static var write: MountFlags { MountFlags(rawValue: _ST_WRITE) } + + /// Files on the file system are append-only. + /// + /// The corresponding C constant is `ST_APPEND`. + /// - Note: Only available on Linux and WASI. + @_alwaysEmitIntoClient + public static var appendOnly: MountFlags { MountFlags(rawValue: _ST_APPEND) } + + /// Files on the file system are immutable. + /// + /// The corresponding C constant is `ST_IMMUTABLE`. + /// - Note: Only available on Linux and WASI. + @_alwaysEmitIntoClient + public static var immutable: MountFlags { MountFlags(rawValue: _ST_IMMUTABLE) } + #endif + + // MARK: Flags Available on Linux, Android, and FreeBSD + + #if os(Linux) || os(Android) || os(FreeBSD) + /// Symbolic links are not followed when resolving paths on the file system. + /// + /// Unlike Darwin's `noSymlinkFollowAtMountPoint`, this suppresses symlink + /// following for all path resolution on the mount, not just when resolving + /// the mount point. + /// + /// The corresponding C constant is `MNT_NOSYMFOLLOW` on FreeBSD, + /// or `ST_NOSYMFOLLOW` on Linux and Android. + /// - Note: Only available on Linux, Android, and FreeBSD. + @_alwaysEmitIntoClient + public static var noSymlinkFollow: MountFlags { MountFlags(rawValue: _MOUNT_NOSYMFOLLOW) } + #endif + + // MARK: Flags Available on Darwin, FreeBSD, and OpenBSD + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + /// The file system is written to asynchronously. + /// + /// The corresponding C constant is `MNT_ASYNC`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public static var asynchronous: MountFlags { MountFlags(rawValue: _MNT_ASYNC) } + + /// The file system is exported for use over the network via NFS. + /// + /// The corresponding C constant is `MNT_EXPORTED`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public static var exported: MountFlags { MountFlags(rawValue: _MNT_EXPORTED) } + + /// The file system is stored locally, rather than being accessed over a network. + /// + /// The corresponding C constant is `MNT_LOCAL`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public static var local: MountFlags { MountFlags(rawValue: _MNT_LOCAL) } + + /// Quotas are enabled on the file system. + /// + /// The corresponding C constant is `MNT_QUOTA`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public static var quota: MountFlags { MountFlags(rawValue: _MNT_QUOTA) } + + /// The file system is the root file system. + /// + /// The corresponding C constant is `MNT_ROOTFS`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public static var rootFileSystem: MountFlags { MountFlags(rawValue: _MNT_ROOTFS) } + #endif + + // MARK: Flags Available on Darwin and FreeBSD + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// The file system is unioned with the underlying file system, rather than + /// obscuring it. + /// + /// The corresponding C constant is `MNT_UNION`. + /// - Note: Only available on Darwin and FreeBSD. + @_alwaysEmitIntoClient + public static var union: MountFlags { MountFlags(rawValue: _MNT_UNION) } + + /// The file system was mounted by the automounter. + /// + /// The corresponding C constant is `MNT_AUTOMOUNTED`. + /// - Note: Only available on Darwin and FreeBSD. See `autofs(4)`. + @_alwaysEmitIntoClient + public static var automounted: MountFlags { MountFlags(rawValue: _MNT_AUTOMOUNTED) } + + /// The file system supports Mandatory Access Control (MAC) labels for + /// individual objects. + /// + /// The corresponding C constant is `MNT_MULTILABEL`. + /// - Note: Only available on Darwin and FreeBSD. + @_alwaysEmitIntoClient + public static var multiLabel: MountFlags { MountFlags(rawValue: _MNT_MULTILABEL) } + #endif + + // MARK: Flags Available on FreeBSD and OpenBSD + + #if os(FreeBSD) || os(OpenBSD) + /// The file system is exported for reading only. + /// + /// The corresponding C constant is `MNT_EXRDONLY`. + /// - Note: Only available on FreeBSD and OpenBSD. + @_alwaysEmitIntoClient + public static var exportedReadOnly: MountFlags { MountFlags(rawValue: _MNT_EXRDONLY) } + + /// The file system is exported for reading and writing to any host by default. + /// + /// The corresponding C constant is `MNT_DEFEXPORTED`. + /// - Note: Only available on FreeBSD and OpenBSD. + @_alwaysEmitIntoClient + public static var exportedByDefault: MountFlags { MountFlags(rawValue: _MNT_DEFEXPORTED) } + + /// The file system maps all remote users to the anonymous user account. + /// + /// The corresponding C constant is `MNT_EXPORTANON`. + /// - Note: Only available on FreeBSD and OpenBSD. + @_alwaysEmitIntoClient + public static var exportedAnonymously: MountFlags { MountFlags(rawValue: _MNT_EXPORTANON) } + + /// The file system uses soft updates. + /// + /// The corresponding C constant is `MNT_SOFTDEP`. + /// - Note: Only available on FreeBSD and OpenBSD. Accepted for compatibility + /// on OpenBSD, but has no effect there. + @_alwaysEmitIntoClient + public static var softUpdates: MountFlags { MountFlags(rawValue: _MNT_SOFTDEP) } + #endif + + // MARK: Flags Available on Darwin Only + + #if SYSTEM_PACKAGE_DARWIN + /// The file system supports per-file encrypted data protection. + /// + /// The corresponding C constant is `MNT_CPROTECT`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var contentProtection: MountFlags { MountFlags(rawValue: _MNT_CPROTECT) } + + /// The file system resides on removable media. + /// + /// The corresponding C constant is `MNT_REMOVABLE`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var removable: MountFlags { MountFlags(rawValue: _MNT_REMOVABLE) } + + /// The file system is quarantined. + /// + /// The corresponding C constant is `MNT_QUARANTINE`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var quarantine: MountFlags { MountFlags(rawValue: _MNT_QUARANTINE) } + + /// The file system supports volfs. + /// + /// The corresponding C constant is `MNT_DOVOLFS`. + /// - Note: Only available on Darwin. Deprecated since Mac OS X 10.5 and + /// not set on modern systems. + @_alwaysEmitIntoClient + public static var volumeFileSystem: MountFlags { MountFlags(rawValue: _MNT_DOVOLFS) } + + /// The file system should not be presented to the user for browsing + /// (e.g. hidden in Finder). + /// + /// The corresponding C constant is `MNT_DONTBROWSE`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var noBrowsing: MountFlags { MountFlags(rawValue: _MNT_DONTBROWSE) } + + /// Ownership information on the file system is ignored. + /// + /// The corresponding C constant is `MNT_IGNORE_OWNERSHIP`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var ignoreOwnership: MountFlags { MountFlags(rawValue: _MNT_IGNORE_OWNERSHIP) } + + /// The file system is journaled. + /// + /// The corresponding C constant is `MNT_JOURNALED`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var journaled: MountFlags { MountFlags(rawValue: _MNT_JOURNALED) } + + /// User extended attributes are not allowed on the file system. + /// + /// The corresponding C constant is `MNT_NOUSERXATTR`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var noUserExtendedAttributes: MountFlags { MountFlags(rawValue: _MNT_NOUSERXATTR) } + + /// The file system defers writes. + /// + /// The corresponding C constant is `MNT_DEFWRITE`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var deferWrites: MountFlags { MountFlags(rawValue: _MNT_DEFWRITE) } + + /// Symbolic links are not followed when resolving the mount point. + /// + /// The corresponding C constant is `MNT_NOFOLLOW`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var noSymlinkFollowAtMountPoint: MountFlags { MountFlags(rawValue: _MNT_NOFOLLOW) } + + /// The mount is a snapshot. + /// + /// The corresponding C constant is `MNT_SNAPSHOT`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var snapshot: MountFlags { MountFlags(rawValue: _MNT_SNAPSHOT) } + + /// Access times are always updated on access. Relatime-style optimizations + /// are disabled. + /// + /// The corresponding C constant is `MNT_STRICTATIME`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var strictAccessTime: MountFlags { MountFlags(rawValue: _MNT_STRICTATIME) } + #endif + + // MARK: Flags Available on FreeBSD Only + + #if os(FreeBSD) + /// The file system is exported with Kerberos user-ID mapping. + /// + /// The corresponding C constant is `MNT_EXKERB`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var exportedKerberos: MountFlags { MountFlags(rawValue: _MNT_EXKERB) } + + /// The file system is exported publicly for WebNFS clients. + /// + /// The corresponding C constant is `MNT_EXPUBLIC`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var exportedPublic: MountFlags { MountFlags(rawValue: _MNT_EXPUBLIC) } + + /// The file system supports POSIX.1e ACLs. + /// + /// The corresponding C constant is `MNT_ACLS`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var posixACLs: MountFlags { MountFlags(rawValue: _MNT_ACLS) } + + /// The file system uses `gjournal`. + /// + /// The corresponding C constant is `MNT_GJOURNAL`. + /// - Note: Only available on FreeBSD. See `gjournal(8)`. + @_alwaysEmitIntoClient + public static var geomJournaled: MountFlags { MountFlags(rawValue: _MNT_GJOURNAL) } + + /// The file system is omitted from `df(1)` listings. + /// + /// The corresponding C constant is `MNT_IGNORE`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var excludedFromDiskFreeReports: MountFlags { MountFlags(rawValue: _MNT_IGNORE) } + + /// The file system supports NFSv4 ACLs. + /// + /// The corresponding C constant is `MNT_NFS4ACLS`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var nfs4ACLs: MountFlags { MountFlags(rawValue: _MNT_NFS4ACLS) } + + /// Clustered reads are disabled on the file system. + /// + /// The corresponding C constant is `MNT_NOCLUSTERR`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var noClusterRead: MountFlags { MountFlags(rawValue: _MNT_NOCLUSTERR) } + + /// Clustered writes are disabled on the file system. + /// + /// The corresponding C constant is `MNT_NOCLUSTERW`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var noClusterWrite: MountFlags { MountFlags(rawValue: _MNT_NOCLUSTERW) } + + /// Newly created files in a directory with the set-user-ID bit set are owned + /// by that directory's owner, rather than by the creating user. + /// + /// The corresponding C constant is `MNT_SUIDDIR`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var setUserIDDirectory: MountFlags { MountFlags(rawValue: _MNT_SUIDDIR) } + + /// The file system uses soft updates with journaling. + /// + /// The corresponding C constant is `MNT_SUJ`. + /// - Note: Only available on FreeBSD. Combines `softUpdates` with a journal + /// for fast recovery. + @_alwaysEmitIntoClient + public static var softUpdateJournaling: MountFlags { MountFlags(rawValue: _MNT_SUJ) } + + /// The file system is untrusted, as the integrity of its media is unknown. + /// + /// The corresponding C constant is `MNT_UNTRUSTED`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var untrusted: MountFlags { MountFlags(rawValue: _MNT_UNTRUSTED) } + + /// The file system was mounted by a non-root user. + /// + /// The corresponding C constant is `MNT_USER`. + /// - Note: Only available on FreeBSD. + @_alwaysEmitIntoClient + public static var mountedByUser: MountFlags { MountFlags(rawValue: _MNT_USER) } + + /// The file system is marked as verified, so per-file integrity checks are + /// skipped on execution. + /// + /// The corresponding C constant is `MNT_VERIFIED`. + /// - Note: Only available on FreeBSD. See `mac_veriexec(4)`. + @_alwaysEmitIntoClient + public static var verified: MountFlags { MountFlags(rawValue: _MNT_VERIFIED) } + #endif + + // MARK: Flags Available on OpenBSD Only + + #if os(OpenBSD) + /// File permissions are not checked on the file system (FFS only). + /// + /// The corresponding C constant is `MNT_NOPERM`. + /// - Note: Only available on OpenBSD. + @_alwaysEmitIntoClient + public static var noPermissionChecks: MountFlags { MountFlags(rawValue: _MNT_NOPERM) } + + /// Programs residing on the file system may create memory mappings that are + /// both writable and executable. + /// + /// By default, requesting such a mapping (via `mmap(2)` or `mprotect(2)`) + /// kills the process. This flag lifts that restriction. + /// + /// The corresponding C constant is `MNT_WXALLOWED`. + /// - Note: Only available on OpenBSD. + @_alwaysEmitIntoClient + public static var writeExecuteAllowed: MountFlags { MountFlags(rawValue: _MNT_WXALLOWED) } + #endif +} + +#endif // !os(Windows) diff --git a/Sources/System/FileSystem/StatFS.swift b/Sources/System/FileSystem/StatFS.swift new file mode 100644 index 00000000..91618f1a --- /dev/null +++ b/Sources/System/FileSystem/StatFS.swift @@ -0,0 +1,749 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2026 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 `statfs` struct on Darwin and BSD operating +/// systems, or the standard `statvfs` otherwise. +/// +/// - Note: Only available on Unix-like platforms. +@available(Windows, unavailable, message: "StatFS is unavailable on Windows. Consider using a Win32 API such as GetVolumeInformationW or GetDiskFreeSpaceExW instead.") +public struct StatFS {} + +extension FileDescriptor { + /// Creates a `StatFS` for the file system containing the file referenced by + /// this `FileDescriptor`. + @available(Windows, unavailable, message: "StatFS is unavailable on Windows. Consider using a Win32 API such as GetVolumeInformationW or GetDiskFreeSpaceExW instead.") + public func statfs(retryOnInterrupt: Bool = true) throws(Errno) -> StatFS { + fatalError("StatFS is unavailable on Windows") + } +} + +extension FilePath { + /// Creates a `StatFS` for the file system containing the file referenced by + /// this `FilePath`. + @available(Windows, unavailable, message: "StatFS is unavailable on Windows. Consider using a Win32 API such as GetVolumeInformationW or GetDiskFreeSpaceExW instead.") + public func statfs(retryOnInterrupt: Bool = true) throws(Errno) -> StatFS { + fatalError("StatFS is unavailable on Windows") + } +} + +#else + +// Must import here to use C statfs/statvfs 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 CSystem +import WASILibc +#elseif canImport(Android) +import CSystem +import Android +#else +#error("Unsupported Platform") +#endif + +// MARK: - FileSystemID + +/// A Swift wrapper of the C `f_fsid` file system ID found in a `statfs` or +/// `statvfs` struct. +@frozen +@available(System 199, *) +public struct FileSystemID: RawRepresentable, Sendable { + + /// The raw C file system ID. + @_alwaysEmitIntoClient + public var rawValue: CInterop.FileSystemID + + /// Creates a strongly-typed `FileSystemID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FileSystemID) { self.rawValue = rawValue } + + /// Creates a strongly-typed `FileSystemID` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.FileSystemID) { self.rawValue = rawValue } +} + +// On Darwin, FreeBSD, and OpenBSD, `CInterop.FileSystemID` is the C `fsid_t` +// struct (a fixed two-element `int32_t` array), which provides no synthesized +// conformances. Implement `Equatable`, `Hashable`, and `Codable` manually in +// terms of the underlying `val` members. On other platforms, the raw value is +// an integer, so the conformances are derived automatically. +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) +@available(System 199, *) +extension FileSystemID: Equatable { + @_alwaysEmitIntoClient + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.rawValue.val.0 == rhs.rawValue.val.0 && lhs.rawValue.val.1 == rhs.rawValue.val.1 + } +} + +@available(System 199, *) +extension FileSystemID: Hashable { + @_alwaysEmitIntoClient + public func hash(into hasher: inout Hasher) { + hasher.combine(rawValue.val.0) + hasher.combine(rawValue.val.1) + } +} + +@available(System 199, *) +extension FileSystemID: Codable { + @_alwaysEmitIntoClient + public func encode(to encoder: any Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(rawValue.val.0) + try container.encode(rawValue.val.1) + } + + @_alwaysEmitIntoClient + public init(from decoder: any Decoder) throws { + var container = try decoder.unkeyedContainer() + let val0 = try container.decode(Int32.self) + let val1 = try container.decode(Int32.self) + self.init(rawValue: CInterop.FileSystemID(val: (val0, val1))) + } +} +#else +@available(System 199, *) +extension FileSystemID: Equatable, Hashable, Codable {} +#endif + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +// MARK: - FileSystemType + +/// A Swift wrapper of the C `f_type` file system type found in a `statfs` +/// struct on Darwin and FreeBSD. +/// +/// - Note: Only available on Darwin and FreeBSD. +@frozen +@available(System 199, *) +public struct FileSystemType: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C file system type. + @_alwaysEmitIntoClient + public var rawValue: UInt32 + + /// Creates a strongly-typed `FileSystemType` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { self.rawValue = rawValue } + + /// Creates a strongly-typed `FileSystemType` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: UInt32) { self.rawValue = rawValue } +} +#endif + +#if SYSTEM_PACKAGE_DARWIN +// MARK: - FileSystemSubtype + +/// A Swift wrapper of the C `f_fssubtype` file system subtype found in a +/// `statfs` struct on Darwin. +/// +/// - Note: Only available on Darwin. +@frozen +@available(System 199, *) +public struct FileSystemSubtype: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C file system subtype. + @_alwaysEmitIntoClient + public var rawValue: UInt32 + + /// Creates a strongly-typed `FileSystemSubtype` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { self.rawValue = rawValue } + + /// Creates a strongly-typed `FileSystemSubtype` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: UInt32) { self.rawValue = rawValue } +} +#endif + +// MARK: - StatFS + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) +// Helpers for reading the fixed-size, NUL-terminated C character buffers +// (`f_fstypename`, `f_mntonname`, `f_mntfromname`) in Darwin/BSD `statfs`. +// If the buffer has no NUL (malformed), the entire buffer is used. +@available(System 199, *) +extension String { + @usableFromInline + internal init(_nullTerminatedBytes buffer: UnsafeRawBufferPointer) { + let bytes = buffer.prefix { $0 != 0 } + self = String(decoding: bytes, as: CInterop.PlatformUnicodeEncoding.self) + } +} + +@available(System 199, *) +extension FilePath { + @usableFromInline + internal init(_nullTerminatedBytes buffer: UnsafeRawBufferPointer) { + let chars = buffer.bindMemory(to: CInterop.PlatformChar.self) + guard let base = chars.baseAddress else { + self = FilePath() + return + } + self = if chars.firstIndex(of: 0) != nil { + FilePath(platformString: base) + } else { + withUnsafeTemporaryAllocation( + of: CInterop.PlatformChar.self, + capacity: chars.count + 1 + ) { terminatedBuffer in + terminatedBuffer.baseAddress!.initialize(from: base, count: chars.count) + terminatedBuffer[chars.count] = 0 + return FilePath(platformString: terminatedBuffer.baseAddress!) + } + } + } +} +#endif + +/// A Swift wrapper of the C `statfs` struct on Darwin and BSD operating +/// systems, or the standard `statvfs` otherwise. +/// +/// - Note: Only available on Unix-like platforms. +/// - Note: The numeric properties clamp to the range of the underlying C +/// field in both directions. Use `rawValue` for exact, unclamped access. +@frozen +@available(System 199, *) +public struct StatFS: RawRepresentable, Sendable, Hashable { + + /// The raw C `statfs` struct on Darwin and BSD, or the `statvfs` struct + /// otherwise. + @_alwaysEmitIntoClient + public var rawValue: CInterop.StatFS + + /// Creates a Swift `StatFS` from the raw C struct. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.StatFS) { self.rawValue = rawValue } + + // MARK: Initializers + + /// Creates a `StatFS` from a `FilePath`. + /// + /// The corresponding C function is `statfs()` on Darwin and BSD, or + /// `statvfs()` otherwise. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._statfs($0, retryOnInterrupt: retryOnInterrupt) + }.get() + } + + /// Creates a `StatFS` from a null-terminated `UnsafePointer` path. + /// + /// The corresponding C function is `statfs()` on Darwin and BSD, or + /// `statvfs()` otherwise. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._statfs( + path, retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _statfs( + _ path: UnsafePointer, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.StatFS() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + system_statfs(path, &result) + #else + system_statvfs(path, &result) + #endif + }.map { result } + } + + /// Creates a `StatFS` from a `FileDescriptor`. + /// + /// The corresponding C function is `fstatfs()` on Darwin and BSD, or + /// `fstatvfs()` otherwise. + @_alwaysEmitIntoClient + public init( + _ fd: FileDescriptor, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstatfs( + fd, retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _fstatfs( + _ fd: FileDescriptor, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.StatFS() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + system_fstatfs(fd.rawValue, &result) + #else + system_fstatvfs(fd.rawValue, &result) + #endif + }.map { result } + } + + // MARK: Properties + + /// File system block size, in bytes. + /// + /// The corresponding C property is `f_bsize`. + /// - Note: On Darwin and BSD, this is the fundamental size for block counts. + /// `statvfs` platforms use `fragmentSize` (`f_frsize`) instead. + @_alwaysEmitIntoClient + public var blockSize: Int { + get { Int(clamping: rawValue.f_bsize) } + set { rawValue.f_bsize = .init(clamping: newValue) } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + /// Block size for optimal data transfer, in bytes. + /// + /// The corresponding C property is `f_iosize`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public var preferredIOBlockSize: Int { + get { Int(clamping: rawValue.f_iosize) } + set { rawValue.f_iosize = .init(clamping: newValue) } + } + #else + /// File system fragment size, in bytes. + /// + /// The corresponding C property is `f_frsize`. + /// - Note: On `statvfs` platforms, this is the fundamental size for block + /// counts. Not present on Darwin or BSD, which use `blockSize` instead. + @_alwaysEmitIntoClient + public var fragmentSize: Int { + get { Int(clamping: rawValue.f_frsize) } + set { rawValue.f_frsize = .init(clamping: newValue) } + } + #endif + + /// The fundamental block size used for space calculations, in bytes. + /// + /// This is `blockSize` on Darwin and BSD (`statfs`), or `fragmentSize` + /// otherwise (`statvfs`). + @_alwaysEmitIntoClient + internal var _fundamentalBlockSize: UInt64 { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + UInt64(clamping: rawValue.f_bsize) + #else + UInt64(clamping: rawValue.f_frsize) + #endif + } + + /// Multiplies a block count by the fundamental block size, saturating to + /// `UInt64.max` on overflow. + @_alwaysEmitIntoClient + internal func _saturatingSpace(_ blocks: UInt64) -> UInt64 { + let (result, overflow) = blocks.multipliedReportingOverflow(by: _fundamentalBlockSize) + return overflow ? .max : result + } + + /// Total number of blocks in the file system. + /// + /// The corresponding C property is `f_blocks`. + /// - Note: In units of `blockSize` on Darwin and BSD (`statfs`), or + /// `fragmentSize` otherwise (`statvfs`). + @_alwaysEmitIntoClient + public var totalBlocks: UInt64 { + get { UInt64(clamping: rawValue.f_blocks) } + set { rawValue.f_blocks = .init(clamping: newValue) } + } + + /// Total size of the file system, in bytes. + /// + /// - Note: Computed for convenience as `totalBlocks` times the fundamental + /// block size (see `totalBlocks`). Saturates to `UInt64.max` on overflow. + @_alwaysEmitIntoClient + public var totalSpace: UInt64 { _saturatingSpace(totalBlocks) } + + /// Number of free blocks in the file system. + /// + /// The corresponding C property is `f_bfree`. + /// - Note: In units of `blockSize` on Darwin and BSD (`statfs`), or + /// `fragmentSize` otherwise (`statvfs`). + @_alwaysEmitIntoClient + public var freeBlocks: UInt64 { + get { UInt64(clamping: rawValue.f_bfree) } + set { rawValue.f_bfree = .init(clamping: newValue) } + } + + /// Free space in the file system, in bytes. + /// + /// - Note: Computed for convenience as `freeBlocks` times the fundamental + /// block size (see `freeBlocks`). Saturates to `UInt64.max` on overflow. + @_alwaysEmitIntoClient + public var freeSpace: UInt64 { _saturatingSpace(freeBlocks) } + + /// Number of free blocks available to non-superuser. + /// + /// The corresponding C property is `f_bavail`. + /// - Note: In units of `blockSize` on Darwin and BSD (`statfs`), or + /// `fragmentSize` otherwise (`statvfs`). On FreeBSD and OpenBSD, the + /// underlying C property is signed; negative values are clamped to 0. + @_alwaysEmitIntoClient + public var availableBlocks: UInt64 { + get { UInt64(clamping: rawValue.f_bavail) } + set { rawValue.f_bavail = .init(clamping: newValue) } + } + + /// Available space in the file system for non-superuser, in bytes. + /// + /// - Note: Computed for convenience as `availableBlocks` times the fundamental + /// block size (see `availableBlocks`). Saturates to `UInt64.max` on overflow. + @_alwaysEmitIntoClient + public var availableSpace: UInt64 { _saturatingSpace(availableBlocks) } + + /// Total number of inodes in the file system. + /// + /// The corresponding C property is `f_files`. + @_alwaysEmitIntoClient + public var totalInodes: UInt64 { + get { UInt64(clamping: rawValue.f_files) } + set { rawValue.f_files = .init(clamping: newValue) } + } + + /// Number of free inodes in the file system. + /// + /// The corresponding C property is `f_ffree`. + /// - Note: On FreeBSD, this reports the inodes available to a non-superuser + /// rather than the total free count, and the underlying C field is signed + /// (negative values are clamped to 0); on other platforms, it is the total + /// number of free inodes. + @_alwaysEmitIntoClient + public var freeInodes: UInt64 { + get { UInt64(clamping: rawValue.f_ffree) } + set { rawValue.f_ffree = .init(clamping: newValue) } + } + + #if !SYSTEM_PACKAGE_DARWIN && !os(FreeBSD) + /// Number of free inodes available to non-superuser. + /// + /// The corresponding C property is `f_favail`, reported on the `statvfs` + /// platforms and by OpenBSD's `statfs`. + /// - Note: Darwin and FreeBSD `statfs` do not report it. On OpenBSD, the + /// underlying C property is signed; negative values are clamped to 0. + @_alwaysEmitIntoClient + public var availableInodes: UInt64 { + get { UInt64(clamping: rawValue.f_favail) } + set { rawValue.f_favail = .init(clamping: newValue) } + } + #endif + + #if !SYSTEM_PACKAGE_DARWIN + /// Maximum length of a file name on the file system, in bytes. + /// + /// The corresponding C property is `f_namemax`, reported on the `statvfs` + /// platforms and by FreeBSD and OpenBSD `statfs`. + /// - Note: Darwin's `statfs` does not report it. + @_alwaysEmitIntoClient + public var maximumNameLength: Int { + get { Int(clamping: rawValue.f_namemax) } + set { rawValue.f_namemax = .init(clamping: newValue) } + } + #endif + + /// File system ID. + /// + /// The corresponding C property is `f_fsid`. + @_alwaysEmitIntoClient + public var fileSystemID: FileSystemID { + get { FileSystemID(rawValue: rawValue.f_fsid) } + set { rawValue.f_fsid = newValue.rawValue } + } + + /// Mount flags indicating the options employed when mounting the file system. + /// + /// The corresponding C property is `f_flags` on Darwin and BSD, or `f_flag` + /// otherwise. + @_alwaysEmitIntoClient + public var mountFlags: MountFlags { + get { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + MountFlags(rawValue: rawValue.f_flags) + #else + MountFlags(rawValue: rawValue.f_flag) + #endif + } + set { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + rawValue.f_flags = newValue.rawValue + #else + rawValue.f_flag = newValue.rawValue + #endif + } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// File system type. + /// + /// The corresponding C property is `f_type`. + /// - Note: Only available on Darwin and FreeBSD, where this is an internal, + /// kernel-assigned VFS type index with no stable, public constants; it is + /// *not* a filesystem magic number like those found in the Linux `statfs`. + /// Prefer `typeName` to identify the file system in a readable format. + @_alwaysEmitIntoClient + public var type: FileSystemType { + get { FileSystemType(rawValue: numericCast(rawValue.f_type)) } + set { rawValue.f_type = numericCast(newValue.rawValue) } + } + #endif + + #if SYSTEM_PACKAGE_DARWIN + /// File system subtype. + /// + /// The corresponding C property is `f_fssubtype`. + /// - Note: Like `type`, this is a numeric value with no stable, public + /// constants. Only available on Darwin. + @_alwaysEmitIntoClient + public var subtype: FileSystemSubtype { + get { FileSystemSubtype(rawValue: numericCast(rawValue.f_fssubtype)) } + set { rawValue.f_fssubtype = numericCast(newValue.rawValue) } + } + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + /// User that mounted the file system. + /// + /// The corresponding C property is `f_owner`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public var owner: UserID { + get { UserID(rawValue: rawValue.f_owner) } + set { rawValue.f_owner = newValue.rawValue } + } + + /// File system type name. + /// + /// The corresponding C property is `f_fstypename`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public var typeName: String { + withUnsafeBytes(of: rawValue.f_fstypename) { + String(_nullTerminatedBytes: $0) + } + } + + /// Directory where the file system is mounted, such as "/System/Volumes/Data". + /// + /// The corresponding C property is `f_mntonname`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public var mountPoint: FilePath { + withUnsafeBytes(of: rawValue.f_mntonname) { + FilePath(_nullTerminatedBytes: $0) + } + } + + /// The source of the mounted file system, such as "/dev/disk3s7". + /// + /// The corresponding C property is `f_mntfromname`. + /// - Note: Only available on Darwin and BSD. + @_alwaysEmitIntoClient + public var mountSource: FilePath { + withUnsafeBytes(of: rawValue.f_mntfromname) { + FilePath(_nullTerminatedBytes: $0) + } + } + #endif +} + +// MARK: - StatFS Equatable & Hashable + +@available(System 199, *) +extension StatFS { + /// Compares the meaningful file-system metadata fields of two `StatFS` values. + /// + /// Reserved/"spare" fields are not compared, and name buffers are compared + /// only up to their NUL terminators. + public static func == (lhs: Self, rhs: Self) -> Bool { + guard lhs.blockSize == rhs.blockSize, + lhs.totalBlocks == rhs.totalBlocks, + lhs.freeBlocks == rhs.freeBlocks, + lhs.availableBlocks == rhs.availableBlocks, + lhs.totalInodes == rhs.totalInodes, + lhs.freeInodes == rhs.freeInodes, + lhs.fileSystemID == rhs.fileSystemID, + lhs.mountFlags == rhs.mountFlags else { + return false + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + guard lhs.preferredIOBlockSize == rhs.preferredIOBlockSize else { + return false + } + #else + guard lhs.fragmentSize == rhs.fragmentSize else { + return false + } + #endif + + #if !SYSTEM_PACKAGE_DARWIN && !os(FreeBSD) + guard lhs.availableInodes == rhs.availableInodes else { + return false + } + #endif + + #if !SYSTEM_PACKAGE_DARWIN + guard lhs.maximumNameLength == rhs.maximumNameLength else { + return false + } + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + guard lhs.type == rhs.type else { + return false + } + #endif + + #if SYSTEM_PACKAGE_DARWIN + guard lhs.subtype == rhs.subtype else { + return false + } + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + guard lhs.owner == rhs.owner, + _nullTerminatedBytesEqual(lhs.rawValue.f_fstypename, + rhs.rawValue.f_fstypename), + _nullTerminatedBytesEqual(lhs.rawValue.f_mntonname, + rhs.rawValue.f_mntonname), + _nullTerminatedBytesEqual(lhs.rawValue.f_mntfromname, + rhs.rawValue.f_mntfromname) else { + return false + } + #endif + + return true + } + + /// Hashes the meaningful file-system metadata fields of a `StatFS` struct. + /// + /// These are the same fields compared by `==`. Reserved/"spare" fields are + /// not hashed, and name buffers are hashed only up to their NUL terminators. + public func hash(into hasher: inout Hasher) { + hasher.combine(blockSize) + hasher.combine(totalBlocks) + hasher.combine(freeBlocks) + hasher.combine(availableBlocks) + hasher.combine(totalInodes) + hasher.combine(freeInodes) + hasher.combine(fileSystemID) + hasher.combine(mountFlags) + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + hasher.combine(preferredIOBlockSize) + #else + hasher.combine(fragmentSize) + #endif + + #if !SYSTEM_PACKAGE_DARWIN && !os(FreeBSD) + hasher.combine(availableInodes) + #endif + + #if !SYSTEM_PACKAGE_DARWIN + hasher.combine(maximumNameLength) + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + hasher.combine(type) + #endif + + #if SYSTEM_PACKAGE_DARWIN + hasher.combine(subtype) + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + hasher.combine(owner) + Self._combineNullTerminatedBytes(rawValue.f_fstypename, into: &hasher) + Self._combineNullTerminatedBytes(rawValue.f_mntonname, into: &hasher) + Self._combineNullTerminatedBytes(rawValue.f_mntfromname, into: &hasher) + #endif + } + + // Compares two fixed-size, NUL-terminated C character buffers (such as + // `f_mntonname`) up to their first NUL terminator. + @inline(__always) + private static func _nullTerminatedBytesEqual(_ lhs: T, _ rhs: T) -> Bool { + withUnsafeBytes(of: lhs) { lhsBytes in + withUnsafeBytes(of: rhs) { rhsBytes in + lhsBytes.prefix { $0 != 0 }.elementsEqual(rhsBytes.prefix { $0 != 0 }) + } + } + } + + // Hashes a fixed-size, NUL-terminated C character buffer (such as + // `f_mntonname`) up to its first NUL terminator. + @inline(__always) + private static func _combineNullTerminatedBytes( + _ value: T, into hasher: inout Hasher + ) { + withUnsafeBytes(of: value) { buffer in + let bytes = buffer.prefix { $0 != 0 } + hasher.combine(bytes: .init(rebasing: bytes)) + } + } +} + +// MARK: - FileDescriptor Extensions + +@available(System 199, *) +extension FileDescriptor { + + /// Creates a `StatFS` for the file system containing the file referenced by + /// this `FileDescriptor`. + /// + /// The corresponding C function is `fstatfs()` on Darwin and BSD, or + /// `fstatvfs()` otherwise. + @_alwaysEmitIntoClient + public func statfs( + retryOnInterrupt: Bool = true + ) throws(Errno) -> StatFS { + try StatFS(self, retryOnInterrupt: retryOnInterrupt) + } +} + +// MARK: - FilePath Extensions + +@available(System 199, *) +extension FilePath { + + /// Creates a `StatFS` for the file system containing the file referenced by + /// this `FilePath`. + /// + /// The corresponding C function is `statfs()` on Darwin and BSD, or + /// `statvfs()` otherwise. + @_alwaysEmitIntoClient + public func statfs( + retryOnInterrupt: Bool = true + ) throws(Errno) -> StatFS { + try StatFS(self, retryOnInterrupt: retryOnInterrupt) + } +} + +#endif // !os(Windows) diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index e6af1b26..66ced03e 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -19,6 +19,7 @@ import Glibc @_implementationOnly import CSystem import Musl #elseif canImport(WASILibc) +import CSystem import WASILibc #elseif canImport(Bionic) import CSystem @@ -116,4 +117,27 @@ extension CInterop { public typealias FileFlags = UInt32 #endif } + +@available(System 199, *) +extension CInterop { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + public typealias StatFS = statfs + #else + public typealias StatFS = statvfs + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(OpenBSD) + public typealias MountFlags = UInt32 + #elseif os(FreeBSD) + public typealias MountFlags = UInt64 + #else + public typealias MountFlags = CUnsignedLong + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + public typealias FileSystemID = fsid_t + #else + public typealias FileSystemID = CUnsignedLong + #endif +} #endif diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index fa76713f..f0c5e810 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -26,6 +26,7 @@ import Musl import CSystem import WASILibc #elseif canImport(Android) +import CSystem import Android #else #error("Unsupported Platform") @@ -799,4 +800,350 @@ internal var _UF_SYSTEM: UInt32 { UInt32(bitPattern: UF_SYSTEM) } internal var _SF_SNAPSHOT: UInt32 { UInt32(bitPattern: SF_SNAPSHOT) } #endif +// MARK: - statfs/statvfs Mount Flags + +// Darwin and BSD (`statfs`) and other platforms (`statvfs`) use different C +// names (`MNT_*` vs `ST_*`) for the flags they share, so flags are exposed +// here under general `_MOUNT_*` names that resolve per platform. + +// MARK: Flags Available on All Platforms + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MOUNT_RDONLY: CInterop.MountFlags { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + CInterop.MountFlags(truncatingIfNeeded: MNT_RDONLY) + #elseif os(Linux) || os(Android) + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_RDONLY()) + #else + CInterop.MountFlags(truncatingIfNeeded: ST_RDONLY) + #endif +} + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MOUNT_SYNCHRONOUS: CInterop.MountFlags { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + CInterop.MountFlags(truncatingIfNeeded: MNT_SYNCHRONOUS) + #elseif os(Linux) || os(Android) + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_SYNCHRONOUS()) + #else + CInterop.MountFlags(truncatingIfNeeded: ST_SYNCHRONOUS) + #endif +} + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MOUNT_NOEXEC: CInterop.MountFlags { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + CInterop.MountFlags(truncatingIfNeeded: MNT_NOEXEC) + #elseif os(Linux) || os(Android) + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_NOEXEC()) + #else + CInterop.MountFlags(truncatingIfNeeded: ST_NOEXEC) + #endif +} + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MOUNT_NOSUID: CInterop.MountFlags { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + CInterop.MountFlags(truncatingIfNeeded: MNT_NOSUID) + #elseif os(Linux) || os(Android) + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_NOSUID()) + #else + CInterop.MountFlags(truncatingIfNeeded: ST_NOSUID) + #endif +} + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MOUNT_NOATIME: CInterop.MountFlags { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + CInterop.MountFlags(truncatingIfNeeded: MNT_NOATIME) + #elseif os(Linux) || os(Android) + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_NOATIME()) + #else + CInterop.MountFlags(truncatingIfNeeded: ST_NOATIME) + #endif +} + +// MARK: Flags Available on All Platforms Except FreeBSD + +#if !os(FreeBSD) +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MOUNT_NODEV: CInterop.MountFlags { + #if SYSTEM_PACKAGE_DARWIN || os(OpenBSD) + CInterop.MountFlags(truncatingIfNeeded: MNT_NODEV) + #elseif os(Linux) || os(Android) + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_NODEV()) + #else + CInterop.MountFlags(truncatingIfNeeded: ST_NODEV) + #endif +} +#endif + +// MARK: Flags Available on Linux, WASI, and Android + +#if os(Linux) || os(WASI) || os(Android) +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _ST_MANDLOCK: CInterop.MountFlags { + #if os(WASI) + CInterop.MountFlags(truncatingIfNeeded: ST_MANDLOCK) + #else + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_MANDLOCK()) + #endif +} + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _ST_NODIRATIME: CInterop.MountFlags { + #if os(WASI) + CInterop.MountFlags(truncatingIfNeeded: ST_NODIRATIME) + #else + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_NODIRATIME()) + #endif +} + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _ST_RELATIME: CInterop.MountFlags { + #if os(WASI) + CInterop.MountFlags(truncatingIfNeeded: ST_RELATIME) + #else + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_RELATIME()) + #endif +} +#endif + +// MARK: Flags Available on Linux and WASI Only + +#if os(Linux) || os(WASI) +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _ST_WRITE: CInterop.MountFlags { + #if os(WASI) + CInterop.MountFlags(truncatingIfNeeded: ST_WRITE) + #else + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_WRITE()) + #endif +} + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _ST_APPEND: CInterop.MountFlags { + #if os(WASI) + CInterop.MountFlags(truncatingIfNeeded: ST_APPEND) + #else + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_APPEND()) + #endif +} + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _ST_IMMUTABLE: CInterop.MountFlags { + #if os(WASI) + CInterop.MountFlags(truncatingIfNeeded: ST_IMMUTABLE) + #else + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_IMMUTABLE()) + #endif +} +#endif + +// MARK: Flags Available on Linux, Android, and FreeBSD + +#if os(Linux) || os(Android) || os(FreeBSD) +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MOUNT_NOSYMFOLLOW: CInterop.MountFlags { + #if os(FreeBSD) + CInterop.MountFlags(truncatingIfNeeded: MNT_NOSYMFOLLOW) + #else + CInterop.MountFlags(truncatingIfNeeded: _system_get_ST_NOSYMFOLLOW()) + #endif +} +#endif + +// MARK: Flags Available on Darwin, FreeBSD, and OpenBSD + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_ASYNC: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_ASYNC) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_EXPORTED: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_EXPORTED) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_LOCAL: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_LOCAL) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_QUOTA: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_QUOTA) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_ROOTFS: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_ROOTFS) } +#endif + +// MARK: Flags Available on Darwin and FreeBSD + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_UNION: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_UNION) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_AUTOMOUNTED: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_AUTOMOUNTED) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_MULTILABEL: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_MULTILABEL) } +#endif + +// MARK: Flags Available on FreeBSD and OpenBSD + +#if os(FreeBSD) || os(OpenBSD) +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_EXRDONLY: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_EXRDONLY) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_DEFEXPORTED: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_DEFEXPORTED) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_EXPORTANON: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_EXPORTANON) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_SOFTDEP: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_SOFTDEP) } +#endif + +// MARK: Flags Available on Darwin Only + +#if SYSTEM_PACKAGE_DARWIN +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_CPROTECT: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_CPROTECT) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_REMOVABLE: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_REMOVABLE) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_QUARANTINE: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_QUARANTINE) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_DOVOLFS: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_DOVOLFS) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_DONTBROWSE: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_DONTBROWSE) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_IGNORE_OWNERSHIP: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_IGNORE_OWNERSHIP) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_JOURNALED: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_JOURNALED) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_NOUSERXATTR: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_NOUSERXATTR) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_DEFWRITE: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_DEFWRITE) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_NOFOLLOW: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_NOFOLLOW) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_SNAPSHOT: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_SNAPSHOT) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_STRICTATIME: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_STRICTATIME) } +#endif + +// MARK: Flags Available on FreeBSD Only + +#if os(FreeBSD) +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_EXKERB: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_EXKERB) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_EXPUBLIC: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_EXPUBLIC) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_ACLS: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_ACLS) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_GJOURNAL: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_GJOURNAL) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_IGNORE: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_IGNORE) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_NFS4ACLS: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_NFS4ACLS) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_NOCLUSTERR: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_NOCLUSTERR) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_NOCLUSTERW: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_NOCLUSTERW) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_SUIDDIR: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_SUIDDIR) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_SUJ: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_SUJ) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_UNTRUSTED: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_UNTRUSTED) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_USER: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_USER) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_VERIFIED: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_VERIFIED) } +#endif + +// MARK: Flags Available on OpenBSD Only + +#if os(OpenBSD) +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_NOPERM: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_NOPERM) } + +@available(System 199, *) +@_alwaysEmitIntoClient +internal var _MNT_WXALLOWED: CInterop.MountFlags { CInterop.MountFlags(truncatingIfNeeded: MNT_WXALLOWED) } +#endif + #endif // !os(Windows) diff --git a/Sources/System/Internals/Exports.swift b/Sources/System/Internals/Exports.swift index aa3506e3..cd0fe340 100644 --- a/Sources/System/Internals/Exports.swift +++ b/Sources/System/Internals/Exports.swift @@ -24,6 +24,7 @@ import Glibc @_implementationOnly import CSystem import Musl #elseif canImport(WASILibc) +@_implementationOnly import CSystem import WASILibc #elseif canImport(Android) @_implementationOnly import CSystem @@ -108,6 +109,26 @@ 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) } + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) +@available(System 199, *) +internal func system_statfs(_ p: UnsafePointer, _ s: inout CInterop.StatFS) -> Int32 { + statfs(p, &s) +} +@available(System 199, *) +internal func system_fstatfs(_ fd: CInt, _ s: inout CInterop.StatFS) -> Int32 { + fstatfs(fd, &s) +} +#else +@available(System 199, *) +internal func system_statvfs(_ p: UnsafePointer, _ s: inout CInterop.StatFS) -> Int32 { + statvfs(p, &s) +} +@available(System 199, *) +internal func system_fstatvfs(_ fd: CInt, _ s: inout CInterop.StatFS) -> Int32 { + fstatvfs(fd, &s) +} +#endif #endif // Convention: `system_platform_foo` is a diff --git a/Tests/SystemTests/StatFSTests.swift b/Tests/SystemTests/StatFSTests.swift new file mode 100644 index 00000000..84c7cfdb --- /dev/null +++ b/Tests/SystemTests/StatFSTests.swift @@ -0,0 +1,475 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2026 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 canImport(Foundation) +import Foundation +#endif + +#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("StatFS") +private struct StatFSTests { + + // On WASI, statvfs/fstatvfs are unconditional stubs that set ENOSYS and + // return -1, so every StatFS initializer throws Errno.noFunction. Skip the + // behavioral tests there. + #if !os(WASI) + + @available(System 199, *) + @Test func initializersAgree() async throws { + try withTemporaryFilePath(basename: "StatFS_initializersAgree") { tempDir in + let fromFilePath = try StatFS(tempDir) + let fromCString = try tempDir.withPlatformString { try StatFS($0) } + let fromFilePathExt = try tempDir.statfs() + + let dirFD = try FileDescriptor.open(tempDir, .readOnly) + defer { try? dirFD.close() } + let fromFD = try StatFS(dirFD) + let fromFDExt = try dirFD.statfs() + + // All construction paths describe the same file system. + #expect(fromFilePath.fileSystemID == fromCString.fileSystemID) + #expect(fromFilePath.fileSystemID == fromFilePathExt.fileSystemID) + #expect(fromFilePath.fileSystemID == fromFD.fileSystemID) + #expect(fromFilePath.fileSystemID == fromFDExt.fileSystemID) + + #expect(fromFilePath.blockSize == fromFD.blockSize) + #expect(fromFilePath.totalBlocks == fromFD.totalBlocks) + #expect(fromFilePath.mountFlags == fromFD.mountFlags) + } + } + + @available(System 199, *) + @Test func spaceAndBlocks() async throws { + try withTemporaryFilePath(basename: "StatFS_spaceAndBlocks") { tempDir in + let statfs = try StatFS(tempDir) + + #expect(statfs.blockSize > 0) + #expect(statfs.totalBlocks > 0) + + // Free space cannot exceed the total, and available (non-superuser) + // cannot exceed the total free. + #expect(statfs.freeBlocks <= statfs.totalBlocks) + #expect(statfs.availableBlocks <= statfs.freeBlocks) + + // Computed sizes are the block count times the block-count unit. + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + let unit = UInt64(statfs.blockSize) + #else + let unit = UInt64(statfs.fragmentSize) + #endif + #expect(statfs.totalSpace == statfs.totalBlocks * unit) + #expect(statfs.freeSpace == statfs.freeBlocks * unit) + #expect(statfs.availableSpace == statfs.availableBlocks * unit) + + #expect(statfs.totalSpace >= statfs.freeSpace) + #expect(statfs.freeSpace >= statfs.availableSpace) + } + } + + @available(System 199, *) + @Test func spaceSaturatesOnOverflow() async throws { + try withTemporaryFilePath(basename: "StatFS_saturates") { tempDir in + var statfs = try StatFS(tempDir) + statfs.totalBlocks = .max + #expect(statfs.blockSize > 1) + // Max blocks times a >1 unit overflows UInt64 and must saturate. + #expect(statfs.totalSpace == .max) + } + } + + @available(System 199, *) + @Test func inodes() async throws { + try withTemporaryFilePath(basename: "StatFS_inodes") { tempDir in + let statfs = try StatFS(tempDir) + // totalInodes can legitimately be 0 on file systems with dynamic inode + // allocation, so only assert the ordering when it is nonzero. + #expect(statfs.freeInodes <= statfs.totalInodes || statfs.totalInodes == 0) + #if !SYSTEM_PACKAGE_DARWIN && !os(FreeBSD) + #expect(statfs.availableInodes <= statfs.freeInodes) + #endif + } + } + + @available(System 199, *) + @Test func readOnlyFlagReflectsWritableFileSystem() async throws { + try withTemporaryFilePath(basename: "StatFS_readOnly") { tempDir in + let statfs = try StatFS(tempDir) + + // Create and write a file in the temp dir. If this succeeds, + // the file system is not read-only... + let probe = tempDir.appending("probe") + let fd = try FileDescriptor.open( + probe, .readWrite, options: .create, permissions: .ownerReadWrite) + defer { try? fd.close() } + try fd.writeAll("probe".utf8) + + // ...so the read-only mount flag must be clear. + #expect(!statfs.mountFlags.contains(.readOnly)) + } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + @available(System 199, *) + @Test func darwinAndBSDFields() async throws { + try withTemporaryFilePath(basename: "StatFS_darwinBSD") { tempDir in + let statfs = try StatFS(tempDir) + #expect(statfs.preferredIOBlockSize > 0) + + // `typeName` is a non-empty, readable file system name. + #expect(!statfs.typeName.isEmpty) + + // `mountPoint` is some absolute path, and `mountSource` is a non-empty + // device path. Note that on Darwin, firmlinks mean the temp dir path is + // not necessarily a lexical prefix of its mount point, so don't assert + // that relationship. + #expect(statfs.mountPoint.isAbsolute) + #expect(!statfs.mountSource.string.isEmpty) + + // statfs of the mount point itself reports that same mount point. + let mountStatFS = try StatFS(statfs.mountPoint) + #expect(mountStatFS.mountPoint == statfs.mountPoint) + #expect(mountStatFS.typeName == statfs.typeName) + } + } + #endif + + #if SYSTEM_PACKAGE_DARWIN + @available(System 199, *) + @Test func typeAndSubtypeRoundTrip() throws { + // On Darwin, `type` and `subtype` are opaque kernel indices with no stable + // public constants, so we can only assert that mutation round-trips. + var statfs = StatFS(rawValue: CInterop.StatFS()) + statfs.type = FileSystemType(rawValue: 42) + #expect(statfs.type == FileSystemType(42)) + statfs.subtype = FileSystemSubtype(rawValue: 7) + #expect(statfs.subtype == FileSystemSubtype(7)) + } + #endif + + @available(System 199, *) + @Test func nonexistentPathThrows() async throws { + #expect(throws: Errno.noSuchFileOrDirectory) { + _ = try StatFS("/var/empty/definitely/does/not/exist") + } + #expect(throws: Errno.noSuchFileOrDirectory) { + _ = try StatFS(FilePath("/var/empty/definitely/does/not/exist")) + } + #expect(throws: Errno.noSuchFileOrDirectory) { + _ = try FilePath("/var/empty/definitely/does/not/exist").statfs() + } + } + + @available(System 199, *) + @Test func badFileDescriptorThrows() async throws { + let badFD = FileDescriptor(rawValue: -1) + #expect(throws: Errno.badFileDescriptor) { + _ = try StatFS(badFD) + } + #expect(throws: Errno.badFileDescriptor) { + _ = try badFD.statfs() + } + } + + @available(System 199, *) + @Test func propertiesMatchRawFields() async throws { + // Verify property mappings from the raw C struct + try withTemporaryFilePath(basename: "StatFS_diff") { tempDir in + var raw = CInterop.StatFS() + try tempDir.withPlatformString { + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + try #require(statfs($0, &raw) == 0, "\(Errno.current)") + #else + try #require(statvfs($0, &raw) == 0, "\(Errno.current)") + #endif + } + let s = StatFS(rawValue: raw) + + #expect(s.blockSize == Int(raw.f_bsize)) + #expect(s.totalBlocks == UInt64(clamping: raw.f_blocks)) + #expect(s.freeBlocks == UInt64(clamping: raw.f_bfree)) + #expect(s.availableBlocks == UInt64(clamping: raw.f_bavail)) + #expect(s.totalInodes == UInt64(clamping: raw.f_files)) + #expect(s.freeInodes == UInt64(clamping: raw.f_ffree)) + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + #expect(s.preferredIOBlockSize == Int(raw.f_iosize)) + #expect(s.owner.rawValue == raw.f_owner) + #expect(s.mountFlags.rawValue == raw.f_flags) + // Decode independently with `String(cString:)`, not the implementation's + // own helper, so this checks the decode rather than restating it. + #expect(s.typeName == withUnsafeBytes(of: raw.f_fstypename) { + $0.withMemoryRebound(to: CChar.self) { String(cString: $0.baseAddress!) } + }) + #expect(s.mountPoint.string == withUnsafeBytes(of: raw.f_mntonname) { + $0.withMemoryRebound(to: CChar.self) { String(cString: $0.baseAddress!) } + }) + #expect(s.mountSource.string == withUnsafeBytes(of: raw.f_mntfromname) { + $0.withMemoryRebound(to: CChar.self) { String(cString: $0.baseAddress!) } + }) + #else + #expect(s.fragmentSize == Int(raw.f_frsize)) + #expect(s.availableInodes == UInt64(clamping: raw.f_favail)) + #expect(s.maximumNameLength == Int(raw.f_namemax)) + #expect(s.mountFlags.rawValue == raw.f_flag) + #endif + } + } + + #endif // !os(WASI) + + @available(System 199, *) + @Test func propertiesAndRawValueRoundTrip() throws { + var raw = CInterop.StatFS() + raw.f_bsize = 4096 + var statfs = StatFS(rawValue: raw) + #expect(statfs.blockSize == 4096) + + statfs.blockSize = 8192 + #expect(statfs.blockSize == 8192) + #expect(statfs.rawValue.f_bsize == 8192) + + statfs.totalBlocks = 123456 + #expect(statfs.totalBlocks == 123456) + #expect(statfs.rawValue.f_blocks == 123456) + + statfs.mountFlags.insert(.readOnly) + #expect(statfs.mountFlags.contains(.readOnly)) + } + + // Setters clamp to the field's range instead of trapping. If the field is + // 64-bit the write round-trips; if it's narrower the value clamps to the + // field's max. Either way, setting `UInt64.max` should never trap or wrap + // to a small value. + @available(System 199, *) + @Test func settersClamp() throws { + var statfs = StatFS(rawValue: CInterop.StatFS()) + + statfs.totalBlocks = .max + #expect(statfs.totalBlocks >= UInt64(UInt32.max)) + + statfs.blockSize = .max + #expect(statfs.blockSize > 0) + + // Writing 0 always fits and reads back exactly. + statfs.freeBlocks = 0 + #expect(statfs.freeBlocks == 0) + } + + @available(System 199, *) + @Test func craftedStructComputesSpace() throws { + var raw = CInterop.StatFS() + // The block-count unit is f_bsize on Darwin/BSD, f_frsize on statvfs. + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + raw.f_bsize = 512 + #else + raw.f_bsize = 4096 // Should not be used in calculations + raw.f_frsize = 512 + #endif + raw.f_blocks = 1000 + raw.f_bfree = 400 + raw.f_bavail = 100 + + let s = StatFS(rawValue: raw) + #expect(s.totalSpace == 1000 * 512) + #expect(s.freeSpace == 400 * 512) + #expect(s.availableSpace == 100 * 512) + } + + #if os(FreeBSD) || os(OpenBSD) + // The block/inode-count fields are signed here; negatives clamp to 0. + @available(System 199, *) + @Test func negativeCountsClampToZero() throws { + var raw = CInterop.StatFS() + raw.f_bsize = 512 + raw.f_bavail = -1 + raw.f_ffree = -1 + + let s = StatFS(rawValue: raw) + #expect(s.availableBlocks == 0) + #expect(s.availableSpace == 0) + #expect(s.freeInodes == 0) + } + #endif + + #if os(Linux) + // /proc files report size 0 via stat, so read until EOF instead of getting + // the size up front. + private func _readEntireFile(_ path: String) throws -> String { + let fd = try FileDescriptor.open(FilePath(path), .readOnly) + defer { try? fd.close() } + var result = [UInt8]() + var chunk = [UInt8](repeating: 0, count: 64 * 1024) + while true { + let n = try chunk.withUnsafeMutableBytes { try fd.read(into: $0) } + if n == 0 { break } + result.append(contentsOf: chunk[..] = [:] + for line in contents.split(separator: "\n") { + let fields = line.split(separator: " ") + guard fields.count >= 4 else { continue } + let mountPoint = String(fields[1]) + // Skip octal-escaped paths (spaces, tabs, etc.). + if mountPoint.contains(where: { $0 == "\\" }) { continue } + mounts[mountPoint] = Set(fields[3].split(separator: ",").map(String.init)) + } + + // Match whole options, so "errors=remount-ro" doesn't look like "ro". + let checks: [(option: String, flag: MountFlags)] = [ + ("ro", .readOnly), + ("noexec", .noExecution), + ("nosuid", .noSetUserID), + ("nodev", .noDevices), + ("noatime", .noAccessTime), + ] + + var verified = 0 + for (mountPoint, options) in mounts { + guard let statfs = try? StatFS(FilePath(mountPoint)) else { continue } + for (option, flag) in checks { + #expect( + statfs.mountFlags.contains(flag) == options.contains(option), + "\(mountPoint): flag \(flag) disagrees with options \(options)" + ) + } + verified += 1 + } + + // Guard against a parser that silently matched nothing: "/" always works. + #expect(verified > 0) + } + #endif + + #if os(WASI) + @available(System 199, *) + @Test func wasiThrowsNoFunction() async throws { + // wasi-libc's statvfs is a stub that always fails with ENOSYS. + #expect(throws: Errno.noFunction) { + _ = try StatFS("/") + } + } + #endif + + @available(System 199, *) + @Test func fileSystemIDConformances() throws { + let a = FileSystemID(rawValue: CInterop.FileSystemID()) + let b = FileSystemID(rawValue: CInterop.FileSystemID()) + #expect(a == b) + #expect(a.hashValue == b.hashValue) + + // Codable round-trips, including the manual fsid_t implementation on + // Darwin and BSD. + #if canImport(Foundation) + let statfs = try? StatFS(FilePath("/")) + if let id = statfs?.fileSystemID { + let data = try JSONEncoder().encode(id) + let decoded = try JSONDecoder().decode(FileSystemID.self, from: data) + #expect(decoded == id) + #expect(decoded.hashValue == id.hashValue) + } + #endif + } + + @available(System 199, *) + @Test func mountFlagsIsOptionSet() throws { + var flags: MountFlags = [.readOnly, .noExecution] + #expect(flags.contains(.readOnly)) + #expect(flags.contains(.noExecution)) + #expect(!flags.contains(.synchronous)) + + flags.remove(.readOnly) + #expect(!flags.contains(.readOnly)) + + let empty = MountFlags(rawValue: 0) + #expect(empty.isEmpty) + } + + #if !os(WASI) + @available(System 199, *) + @Test func equalityAndHashing() throws { + try withTemporaryFilePath(basename: "StatFS_equality") { tempDir in + let statfs = try StatFS(tempDir) + let copy = statfs + #expect(statfs == copy) + #expect(statfs.hashValue == copy.hashValue) + #expect(Set([statfs, copy]).count == 1) + + var mutated = statfs + mutated.totalBlocks &+= 1 + #expect(statfs != mutated) + + // Reserved fields are not part of the value. + #if SYSTEM_PACKAGE_DARWIN + var reserved = statfs + reserved.rawValue.f_reserved.0 &+= 1 + #expect(statfs == reserved) + #expect(statfs.hashValue == reserved.hashValue) + #endif + + // Name buffers are read only up to their NUL terminator, so bytes past + // it don't affect the value. + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + var trailing = statfs + try withUnsafeMutableBytes(of: &trailing.rawValue.f_mntonname) { buffer in + // The kernel always NUL-terminates the name buffers. + let terminator = try #require(buffer.firstIndex(of: 0)) + if terminator + 1 < buffer.count { + buffer[terminator + 1] &+= 1 + } + } + #expect(statfs == trailing) + #expect(statfs.hashValue == trailing.hashValue) + #endif + } + } + #endif + +} + +#endif