diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..70e4f6ca --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,13 @@ +# Lines starting with '#' are comments. +# Each line is a case-sensitive file pattern followed by one or more owners. +# Order is important. The last matching pattern has the most precedence. +# More information: https://docs.github.com/en/articles/about-code-owners +# +# Please mirror the repository's file hierarchy in case-sensitive lexicographic +# order. + +# Default owners +* @glessard @lorentey @milseman + +# Swift CI configuration files +.swiftci/ @shahmishal diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..1b78b37d --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,77 @@ +name: Pull request + +permissions: + contents: read + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + tests: + name: Test + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI" + enable_linux_checks: true + linux_os_versions: '["noble", "jammy", "focal"]' + linux_exclude_swift_versions: | + [ + {"swift_version": "5.9"}, + {"swift_version": "5.10"}, + {"os_version": "focal", "swift_version": "nightly-6.2"}, + {"os_version": "focal", "swift_version": "6.2"}, + {"os_version": "focal", "swift_version": "nightly-main"}, + ] + enable_macos_checks: true + macos_exclude_xcode_versions: '[]' + enable_windows_checks: true + windows_exclude_swift_versions: | + [ + {"swift_version": "5.9"}, + {"swift_version": "5.10"} + ] + enable_linux_static_sdk_build: true + linux_static_sdk_exclude_swift_versions: | + [ + {"os_version": "focal", "swift_version": "nightly-6.2"}, + {"os_version": "focal", "swift_version": "6.2"}, + {"os_version": "focal", "swift_version": "nightly-main"}, + ] + enable_wasm_sdk_build: true + wasm_exclude_swift_versions: | + [ + {"os_version": "focal", "swift_version": "nightly-6.2"}, + {"os_version": "focal", "swift_version": "6.2"}, + {"os_version": "focal", "swift_version": "nightly-main"}, + ] + enable_android_sdk_build: true + android_exclude_swift_versions: | + [ + {"os_version": "focal", "swift_version": "nightly-main"}, + ] + + build-abi-stable: + name: Build ABI Stable + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + enable_linux_checks: false + enable_macos_checks: true + enable_windows_checks: false + # Only build + macos_build_command: "xcrun swift build --build-tests" + macos_exclude_xcode_versions: '[]' + # Enable availability to match ABI stable verion of system. + swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI -Xbuild-tools-swiftc -DSYSTEM_ABI_STABLE" + + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "Swift.org" + # https://github.com/apple/swift-system/issues/224 + docs_check_enabled: false + unacceptable_language_check_enabled: false + license_header_check_enabled: false + format_check_enabled: false + python_lint_check_enabled: false diff --git a/.gitignore b/.gitignore index 95c43209..51ae994a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ .DS_Store +/.swiftpm /.build /Packages /*.xcodeproj xcuserdata/ +.*.sw? +/.swiftpm +.docc-build diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 00000000..b9cbb716 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [SystemPackage] diff --git a/.swiftci/5_10_ubuntu2204 b/.swiftci/5_10_ubuntu2204 new file mode 100644 index 00000000..0c65ec16 --- /dev/null +++ b/.swiftci/5_10_ubuntu2204 @@ -0,0 +1,5 @@ +LinuxSwiftPackageJob { + swift_version_tag = "5.10-jammy" + repo = "swift-system" + branch = "main" +} diff --git a/.swiftci/5_9_ubuntu2204 b/.swiftci/5_9_ubuntu2204 new file mode 100644 index 00000000..1ede34f1 --- /dev/null +++ b/.swiftci/5_9_ubuntu2204 @@ -0,0 +1,5 @@ +LinuxSwiftPackageJob { + swift_version_tag = "5.9-jammy" + repo = "swift-system" + branch = "main" +} diff --git a/.swiftci/nightly_6_0_macos b/.swiftci/nightly_6_0_macos new file mode 100644 index 00000000..9acdfa20 --- /dev/null +++ b/.swiftci/nightly_6_0_macos @@ -0,0 +1,5 @@ +macOSSwiftPackageJob { + swift_version = "6.0" + repo = "swift-system" + branch = "main" +} diff --git a/.swiftci/nightly_6_0_ubuntu2204 b/.swiftci/nightly_6_0_ubuntu2204 new file mode 100644 index 00000000..aaa4671a --- /dev/null +++ b/.swiftci/nightly_6_0_ubuntu2204 @@ -0,0 +1,5 @@ +LinuxSwiftPackageJob { + nightly_docker_tag = "nightly-6.0-jammy" + repo = "swift-system" + branch = "main" +} diff --git a/.swiftci/nightly_main_macos b/.swiftci/nightly_main_macos new file mode 100644 index 00000000..329aef79 --- /dev/null +++ b/.swiftci/nightly_main_macos @@ -0,0 +1,5 @@ +macOSSwiftPackageJob { + swift_version = "main" + repo = "swift-system" + branch = "main" +} diff --git a/.swiftci/nightly_main_ubuntu2204 b/.swiftci/nightly_main_ubuntu2204 new file mode 100644 index 00000000..7e03ac06 --- /dev/null +++ b/.swiftci/nightly_main_ubuntu2204 @@ -0,0 +1,5 @@ +LinuxSwiftPackageJob { + nightly_docker_tag = "nightly-jammy" + repo = "swift-system" + branch = "main" +} diff --git a/.swiftci/nightly_main_windows b/.swiftci/nightly_main_windows new file mode 100644 index 00000000..b3d52fd5 --- /dev/null +++ b/.swiftci/nightly_main_windows @@ -0,0 +1,7 @@ +WindowsSwiftPackageWithDockerImageJob { + docker_image = "swiftlang/swift:nightly-windowsservercore-1809" + repo = "swift-system" + branch = "main" + sub_dir = "swift-system" + label = "windows-server-2019" +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..11347f59 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,26 @@ +#[[ +This source file is part of the Swift System open source project + +Copyright (c) 2020 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 +#]] + +cmake_minimum_required(VERSION 3.16.0) +project(SwiftSystem + LANGUAGES C Swift) + +include(GNUInstallDirs) + +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/modules) + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift) + +include(SwiftSupport) + +add_subdirectory(Sources) +add_subdirectory(cmake/modules) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..95c80e78 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,38 @@ +# Code of Conduct + +To be a truly great community, Swift.org needs to welcome developers from all walks of life, with different backgrounds, and with a wide range of experience. A diverse and friendly community will have more great ideas, more unique perspectives, and produce more great code. We will work diligently to make the Swift community welcoming to everyone. + +To give clarity of what is expected of our members, this code of conduct is based on [contributor-covenant.org](http://contributor-covenant.org). This document is used across many open source communities, and we think it articulates our values well. + +### Contributor Code of Conduct v1.4 + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language (e.g., prefer non-gendered words like “folks” to “guys”, non-ableist words like “soundness check” to “sanity check”, etc.) +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others’ private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +This Code of Conduct applies within all project spaces managed by Swift.org, including (but not limited to) source code repositories, bug trackers, web sites, documentation, and online forums. It also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a member of the [Swift Core Team](https://swift.org/community/#community-structure) or by flagging the behavior for moderation (e.g., in the Forums), whether you are the target of that behavior or not. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. The site of the disputed behavior is usually not an acceptable place to discuss moderation decisions, and moderators may move or remove any such discussion. + +Project maintainers are held to a higher standard, and project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership. +If you disagree with a moderation action, you can appeal to the Core Team (or individual Core Team members) privately. + +This policy is adapted from the Contributor Code of Conduct [version 1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/). diff --git a/Package.swift b/Package.swift index 8e20419b..c7621c05 100644 --- a/Package.swift +++ b/Package.swift @@ -1,42 +1,146 @@ -// swift-tools-version:5.2 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version:6.0 +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2020 - 2024 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 +// +//===----------------------------------------------------------------------===// -/* - This source file is part of the Swift System open source project +import PackageDescription - Copyright (c) 2020 Apple Inc. and the Swift System project authors - Licensed under Apache License v2.0 with Runtime Library Exception +struct Available { + var name: String + var version: String + var osAvailability: String + var sourceAvailability: String - See https://swift.org/LICENSE.txt for license information -*/ + init( + _ version: String, + _ osAvailability: String + ) { + self.name = "System" + self.version = version + self.osAvailability = osAvailability + self.sourceAvailability = "macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, visionOS 1.0" + } -import PackageDescription + var swiftSetting: SwiftSetting { + #if SYSTEM_ABI_STABLE + // Use availability matching Darwin API. + let availability = self.osAvailability + #else + // Use availability matching SwiftPM default. + let availability = self.sourceAvailability + #endif + return .enableExperimentalFeature( + "AvailabilityMacro=\(self.name) \(version):\(availability)") + } +} + +let availability: [Available] = [ + Available("0.0.1", "macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0"), + + Available("0.0.2", "macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0"), + + Available("0.0.3", "macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4"), + Available("1.1.0", "macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4"), + + Available("1.1.1", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4"), + Available("1.2.0", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4"), + + Available("1.2.1", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4"), + Available("1.3.0", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4"), + + Available("1.3.1", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4, visionOS 1.0"), + Available("1.3.2", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4, visionOS 1.0"), + Available("1.4.0", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4, visionOS 1.0"), + + Available("1.4.1", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + Available("1.4.2", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + Available("1.5.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + Available("1.6.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + Available("1.6.1", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + + Available("99", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), +] + +let swiftSettingsAvailability = availability.map(\.swiftSetting) + +#if SYSTEM_CI +let swiftSettingsCI: [SwiftSetting] = [ + .unsafeFlags(["-require-explicit-availability=error"]), +] +#else +let swiftSettingsCI: [SwiftSetting] = [] +#endif + +let swiftSettings = swiftSettingsAvailability + swiftSettingsCI + [ + .define( + "SYSTEM_PACKAGE_DARWIN", + .when(platforms: [.macOS, .macCatalyst, .iOS, .watchOS, .tvOS, .visionOS])), + .define("SYSTEM_PACKAGE"), + .define("ENABLE_MOCKING", .when(configuration: .debug)), + .enableExperimentalFeature("Lifetimes"), +] + +let cSettings: [CSetting] = [ + .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), +] -let targets: [PackageDescription.Target] = [ - .target( - name: "SystemPackage", - dependencies: ["SystemInternals"], - path: "Sources/System"), - .target( - name: "SystemInternals", - dependencies: ["CSystem"], - swiftSettings: [ - .define("ENABLE_MOCKING", .when(configuration: .debug)) - ]), - .target( - name: "CSystem", - dependencies: []), - - .testTarget( - name: "SystemTests", - dependencies: ["SystemPackage"]), +#if SYSTEM_ABI_STABLE +let platforms: [SupportedPlatform] = [ + .macOS("26"), + .iOS("26"), + .watchOS("26"), + .tvOS("26"), + .visionOS("26"), ] +#else +let platforms: [SupportedPlatform]? = nil +#endif + +#if os(Linux) +let filesToExclude = ["CMakeLists.txt"] +#else +let filesToExclude = ["CMakeLists.txt", "IORing"] +#endif + +#if os(Linux) +let testsToExclude:[String] = [] +#else +let testsToExclude = ["IORequestTests.swift", "IORingTests.swift"] +#endif let package = Package( - name: "swift-system", - products: [ - .library(name: "SystemPackage", targets: ["SystemPackage"]), - ], - dependencies: [], - targets: targets + name: "swift-system", + platforms: platforms, + products: [ + .library(name: "SystemPackage", targets: ["SystemPackage"]), + ], + dependencies: [], + targets: [ + .target( + name: "CSystem", + dependencies: [], + exclude: ["CMakeLists.txt"], + cSettings: cSettings), + .target( + name: "SystemPackage", + dependencies: ["CSystem"], + path: "Sources/System", + exclude: filesToExclude, + cSettings: cSettings, + swiftSettings: swiftSettings), + .testTarget( + name: "SystemTests", + dependencies: ["SystemPackage"], + exclude: testsToExclude, + cSettings: cSettings, + swiftSettings: swiftSettings), + ], + swiftLanguageVersions: [.v5] ) diff --git a/Proposals/0006-system-stat.md b/Proposals/0006-system-stat.md new file mode 100644 index 00000000..39c1bd3a --- /dev/null +++ b/Proposals/0006-system-stat.md @@ -0,0 +1,982 @@ +# Stat for Swift System + +* 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** +* 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. + +## 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("Size allocated: \(stat.sizeAllocated) 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 `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 + /// + /// 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 named pipe) + /// + /// The corresponding C constant is `S_IFIFO`. + public static var fifo: 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 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 `ALLPERMS`. + 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) + 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) +} +``` + +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 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 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 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. + /// + /// 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. + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) + 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 + /// + /// - 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 + /// + /// 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 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 } + + /// 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 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 } + + // 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). + 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). + 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). + public var st_ctim: timespec { get set } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Time of file creation, given as a C `timespec` since the Epoch. + /// + /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). + /// - Note: Only available on Darwin and FreeBSD. + public var st_birthtim: timespec { 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 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 +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` 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. + +`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 + +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` | " | " | " | " | " | +| `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 | + +#### `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 | +| `hidden` | `UF_HIDDEN` | `UF_HIDDEN` | N/A | +| `systemNoUnlink` | `SF_NOUNLINK` | `SF_NOUNLINK` | N/A | +| `compressed` | `UF_COMPRESSED` | N/A | N/A | +| `tracked` | `UF_TRACKED` | N/A | N/A | +| `dataVault` | `UF_DATAVAULT` | N/A | N/A | +| `restricted` | `SF_RESTRICTED` | N/A | N/A | +| `firmlink` | `SF_FIRMLINK` | N/A | N/A | +| `dataless` | `SF_DATALESS` | N/A | N/A | +| `userNoUnlink` | N/A | `UF_NOUNLINK` | N/A | +| `offline` | N/A | `UF_OFFLINE` | N/A | +| `readOnly` | N/A | `UF_READONLY` | N/A | +| `reparse` | N/A | `UF_REPARSE` | N/A | +| `sparse` | N/A | `UF_SPARSE` | N/A | +| `system` | N/A | `UF_SYSTEM` | N/A | +| `snapshot` | N/A | `SF_SNAPSHOT` | N/A | diff --git a/README.md b/README.md index dd8c634a..0b75fd04 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ Swift System provides idiomatic interfaces to system calls and low-level currency types. Our vision is for System to act as the single home for low-level system interfaces for all supported Swift platforms. -## Multi-platform not Cross-platform +## No Cross-platform Abstractions -System is a multi-platform library, not a cross-platform one. It provides a separate set of APIs and behaviors on every supported platform, closely reflecting the underlying OS interfaces. A single import will pull in the native platform interfaces specific for the targeted OS. +Swift System is not a cross-platform library. It provides a separate set of APIs and behaviors on every supported platform, closely reflecting the underlying OS interfaces. A single import will pull in the native platform interfaces specific for the targeted OS. -Our immediate goal is to simplify building cross-platform libraries and applications such as SwiftNIO and SwiftPM. System does not eliminate the need for `#if os()` conditionals to implement cross-platform abstractions, but it does make it safer and more expressive to fill out the platform-specific parts. +Our immediate goal is to simplify building cross-platform libraries and applications such as SwiftNIO and SwiftPM. It is not a design goal for System to eliminate the need for `#if os()` conditionals to implement cross-platform abstractions; rather, our goal is to make it safer and more expressive to fill out the platform-specific parts. + +(That said, it is desirable to avoid unnecessary differences -- for example, when two operating systems share the same C name for a system call, ideally Swift System would expose them using the same Swift name. This is a particularly obvious expectation for system interfaces that implement an industry standard, such as POSIX.) ## Usage @@ -22,22 +24,15 @@ try fd.closeAfter { } ``` +[API documentation](https://swiftpackageindex.com/apple/swift-system/documentation/SystemPackage) + ## Adding `SystemPackage` as a Dependency To use the `SystemPackage` library in a SwiftPM project, add the following line to the dependencies in your `Package.swift` file: ```swift -.package(url: "https://github.com/apple/swift-system", from: "0.0.1"), -``` - -Because `SystemPackage` is under active development, -source-stability is only guaranteed within minor versions (e.g. between `0.0.3` and `0.0.4`). -If you don't want potentially source-breaking package updates, -use this dependency specification instead: - -```swift -.package(url: "https://github.com/apple/swift-system", .upToNextMinor(from: "0.0.1")), +.package(url: "https://github.com/apple/swift-system", from: "1.6.1"), ``` Finally, include `"SystemPackage"` as a dependency for your executable target: @@ -46,7 +41,7 @@ Finally, include `"SystemPackage"` as a dependency for your executable target: let package = Package( // name, platforms, products, etc. dependencies: [ - .package(url: "https://github.com/apple/swift-system", from: "0.0.1"), + .package(url: "https://github.com/apple/swift-system", from: "1.6.1"), // other dependencies ], targets: [ @@ -57,3 +52,75 @@ let package = Package( ] ) ``` + +## Source Stability + +At this time, the Swift System package supports three types of operating systems: Darwin-based, POSIX-like, and Windows. The source-stability status of the package differs according to the platform: + +| Platform type | Source Stability | +| ----------------- | --------------- | +| Darwin (macOS, iOS, etc.) | Stable | +| POSIX (Linux, WASI, etc.) | Stable | +| Windows | Unstable | + +The package version numbers follow [Semantic Versioning][semver] -- source breaking changes to source-stable public API can only land in a new major version. However, platforms for which support has not reached source stability may see source-breaking changes in a new minor version. + +[semver]: https://semver.org + +The public API of the swift-system package consists of non-underscored declarations that are marked `public` in the `SystemPackage` module. + +By "underscored declarations" we mean declarations that have a leading underscore anywhere in their fully qualified name. For instance, here are some names that wouldn't be considered part of the public API, even if they were technically marked public: + +- `FooModule.Bar._someMember(value:)` (underscored member) +- `FooModule._Bar.someMember` (underscored type) +- `_FooModule.Bar` (underscored module) +- `FooModule.Bar.init(_value:)` (underscored initializer) + +Interfaces that aren't part of the public API may continue to change in any release, including patch releases. If you have a use case that requires using non-public APIs, please submit a Feature Request describing it! We'd like the public interface to be as useful as possible -- although preferably without compromising safety or limiting future evolution. + +Future minor versions of the package may update these rules as needed. + +## Toolchain Requirements + +The following table maps existing package releases to their minimum required Swift toolchain release: + +| Package version | Swift version | Xcode release | +| ----------------------- | --------------- | ------------- | +| swift-system 1.3.x | >= Swift 5.8 | >= Xcode 14.3 | +| swift-system 1.4.x through 1.6.x | >= Swift 5.9 | >= Xcode 15.0 | + +We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. Accordingly, from time to time, new versions of this package require clients to upgrade to a more recent Swift toolchain release. (This allows the package to make use of new language/stdlib features, build on compiler bug fixes, and adopt new package manager functionality as soon as they are available.) Patch (i.e., bugfix) releases will not increase the required toolchain version, but any minor (i.e., new feature) release may do so. + +(Note: the package has no minimum deployment target, so while it does require clients to use a recent Swift toolchain to build it, the code itself is able to run on any OS release that supports running Swift code.) + +## Licensing + +See [LICENSE](LICENSE.txt) for license information. + +## Contributing + +Before contributing, please read [CONTRIBUTING.md](CONTRIBUTING.md). + +### Branching Strategy + +We maintain separate branches for each active minor version of the package: + +| Package version | Branch | +| ----------------------- | ----------- | +| swift-system 1.3.x | release/1.3 | +| swift-system 1.4.x | release/1.4 | +| swift-system 1.5.x | release/1.5 | +| swift-system 1.6.x | release/1.6 | +| swift-system 1.7.x (unreleased) | main | + +Changes must land on the branch corresponding to the earliest release that they will need to ship on. They are periodically propagated to subsequent branches, in the following direction: + +`release/1.3` → `release/1.4` → `main` + +For example, anything landing on `release/1.3` will eventually appear on `release/1.4` and then `main` too; there is no need to file standalone PRs for each release line. (Change propagation currently requires manual work -- it is performed by project maintainers.) + +### Code of Conduct + +Like all Swift.org projects, we would like the Swift System project to foster a diverse and friendly community. We expect contributors to adhere to the [Swift.org Code of Conduct](https://swift.org/code-of-conduct/). A copy of this document is [available in this repository][coc]. + +[coc]: CODE_OF_CONDUCT.md diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt new file mode 100644 index 00000000..d788b8c3 --- /dev/null +++ b/Sources/CMakeLists.txt @@ -0,0 +1,11 @@ +#[[ +This source file is part of the Swift System open source project + +Copyright (c) 2020 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 +#]] + +add_subdirectory(CSystem) +add_subdirectory(System) diff --git a/Sources/CSystem/CMakeLists.txt b/Sources/CSystem/CMakeLists.txt new file mode 100644 index 00000000..faf730e3 --- /dev/null +++ b/Sources/CSystem/CMakeLists.txt @@ -0,0 +1,20 @@ +#[[ +This source file is part of the Swift System open source project + +Copyright (c) 2020 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 +#]] + +add_library(CSystem INTERFACE) +target_include_directories(CSystem INTERFACE + "$" + $) + +install(FILES + include/CSystemLinux.h + include/CSystemWindows.h + include/module.modulemap + DESTINATION include/CSystem) +set_property(GLOBAL APPEND PROPERTY SWIFT_SYSTEM_EXPORTS CSystem) diff --git a/Sources/CSystem/include/CSystemLinux.h b/Sources/CSystem/include/CSystemLinux.h index b172d658..6489c4f3 100644 --- a/Sources/CSystem/include/CSystemLinux.h +++ b/Sources/CSystem/include/CSystemLinux.h @@ -21,5 +21,6 @@ #include #include #include +#include "io_uring.h" #endif diff --git a/Sources/CSystem/include/CSystemWASI.h b/Sources/CSystem/include/CSystemWASI.h new file mode 100644 index 00000000..1c8cd0f2 --- /dev/null +++ b/Sources/CSystem/include/CSystemWASI.h @@ -0,0 +1,66 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 - 2025 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +#pragma once + +#if __wasi__ + +#include +#include +#include +#include // For NAME_MAX + +// 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; } +static inline int32_t _getConst_O_APPEND(void) { return O_APPEND; } +static inline int32_t _getConst_O_CREAT(void) { return O_CREAT; } +static inline int32_t _getConst_O_DIRECTORY(void) { return O_DIRECTORY; } +static inline int32_t _getConst_O_EXCL(void) { return O_EXCL; } +static inline int32_t _getConst_O_NONBLOCK(void) { return O_NONBLOCK; } +static inline int32_t _getConst_O_TRUNC(void) { return O_TRUNC; } +static inline int32_t _getConst_O_WRONLY(void) { return O_WRONLY; } + +static inline int32_t _getConst_EWOULDBLOCK(void) { return EWOULDBLOCK; } +static inline int32_t _getConst_EOPNOTSUPP(void) { return EOPNOTSUPP; } + +static inline uint8_t _getConst_DT_DIR(void) { return DT_DIR; } + +// Modified dirent struct that can be imported to Swift +struct _system_dirent { + ino_t d_ino; + unsigned char d_type; + // char d_name[] cannot be imported to Swift + char d_name[NAME_MAX + 1]; +}; + +// Convert WASI dirent with d_name[] to _system_dirent +static inline +struct _system_dirent * +_system_dirent_from_wasi_dirent(const struct dirent *wasi_dirent) { + + // Match readdir behavior and use thread-local storage for the converted dirent + static __thread struct _system_dirent _converted_dirent; + + if (wasi_dirent == NULL) { + return NULL; + } + + memset(&_converted_dirent, 0, sizeof(struct _system_dirent)); + + _converted_dirent.d_ino = wasi_dirent->d_ino; + _converted_dirent.d_type = wasi_dirent->d_type; + + strncpy(_converted_dirent.d_name, wasi_dirent->d_name, NAME_MAX); + _converted_dirent.d_name[NAME_MAX] = '\0'; + + return &_converted_dirent; +} + +#endif diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h new file mode 100644 index 00000000..ce618f3a --- /dev/null +++ b/Sources/CSystem/include/io_uring.h @@ -0,0 +1,247 @@ +#include +#include +#include +#include + +#define __SWIFT_IORING_SQE_FALLBACK_STRUCT { \ + __u8 opcode; \ + __u8 flags; \ + __u16 ioprio; \ + __s32 fd; \ + union { \ + __u64 off; \ + __u64 addr2; \ + struct { \ + __u32 cmd_op; \ + __u32 __pad1; \ + }; \ + }; \ + union { \ + __u64 addr; \ + __u64 splice_off_in; \ + struct { \ + __u32 level; \ + __u32 optname; \ + }; \ + }; \ + __u32 len; \ + union { \ + __kernel_rwf_t rw_flags; \ + __u32 fsync_flags; \ + __u16 poll_events; \ + __u32 poll32_events; \ + __u32 sync_range_flags; \ + __u32 msg_flags; \ + __u32 timeout_flags; \ + __u32 accept_flags; \ + __u32 cancel_flags; \ + __u32 open_flags; \ + __u32 statx_flags; \ + __u32 fadvise_advice; \ + __u32 splice_flags; \ + __u32 rename_flags; \ + __u32 unlink_flags; \ + __u32 hardlink_flags; \ + __u32 xattr_flags; \ + __u32 msg_ring_flags; \ + __u32 uring_cmd_flags; \ + __u32 waitid_flags; \ + __u32 futex_flags; \ + __u32 install_fd_flags; \ + __u32 nop_flags; \ + }; \ + __u64 user_data; \ + union { \ + __u16 buf_index; \ + __u16 buf_group; \ + } __attribute__((packed)); \ + __u16 personality; \ + union { \ + __s32 splice_fd_in; \ + __u32 file_index; \ + __u32 optlen; \ + struct { \ + __u16 addr_len; \ + __u16 __pad3[1]; \ + }; \ + }; \ + union { \ + struct { \ + __u64 addr3; \ + __u64 __pad2[1]; \ + }; \ + __u64 optval; \ + __u8 cmd[0]; \ + }; \ +} + +#if __has_include() +#include + +#ifdef IORING_TIMEOUT_BOOTTIME +// Kernel version >= 5.15, io_uring_sqe has file_index +// and all current Swift operations are supported. +#define __SWIFT_IORING_SUPPORTED true +typedef struct io_uring_sqe swift_io_uring_sqe; +#else +// io_uring_sqe is missing properties that IORequest expects. +// This configuration is not supported for now. +// +// Define a fallback struct to avoid build errors, but IORing +// will throw ENOTSUP on initialization. +#define __SWIFT_IORING_SUPPORTED false +typedef struct __SWIFT_IORING_SQE_FALLBACK_STRUCT swift_io_uring_sqe; +#endif + +// We can define more specific availability later + +#ifdef IORING_FEAT_RW_CUR_POS +// Kernel version >= 5.6, io_uring_sqe has open_flags +#endif + +#ifdef IORING_FEAT_NODROP +// Kernel version >= 5.5, io_uring_sqe has cancel_flags +#endif + +#else +// Minimal fallback definitions when linux/io_uring.h is not available (e.g. static SDK) +#include + +#define __SWIFT_IORING_SUPPORTED false + +#define IORING_OFF_SQ_RING 0ULL +#define IORING_OFF_CQ_RING 0x8000000ULL +#define IORING_OFF_SQES 0x10000000ULL + +#define IORING_ENTER_GETEVENTS (1U << 0) + +#define IORING_FEAT_SINGLE_MMAP (1U << 0) +#define IORING_FEAT_NODROP (1U << 1) +#define IORING_FEAT_SUBMIT_STABLE (1U << 2) +#define IORING_FEAT_RW_CUR_POS (1U << 3) +#define IORING_FEAT_CUR_PERSONALITY (1U << 4) +#define IORING_FEAT_FAST_POLL (1U << 5) +#define IORING_FEAT_POLL_32BITS (1U << 6) +#define IORING_FEAT_SQPOLL_NONFIXED (1U << 7) +#define IORING_FEAT_EXT_ARG (1U << 8) +#define IORING_FEAT_NATIVE_WORKERS (1U << 9) +#define IORING_FEAT_RSRC_TAGS (1U << 10) +#define IORING_FEAT_CQE_SKIP (1U << 11) +#define IORING_FEAT_LINKED_FILE (1U << 12) +#define IORING_FEAT_REG_REG_RING (1U << 13) +#define IORING_FEAT_RECVSEND_BUNDLE (1U << 14) +#define IORING_FEAT_MIN_TIMEOUT (1U << 15) +#define IORING_FEAT_RW_ATTR (1U << 16) +#define IORING_FEAT_NO_IOWAIT (1U << 17) + +#if !defined(_ASM_GENERIC_INT_LL64_H) && !defined(_ASM_GENERIC_INT_L64_H) && !defined(_UAPI_ASM_GENERIC_INT_LL64_H) && !defined(_UAPI_ASM_GENERIC_INT_L64_H) +typedef uint8_t __u8; +typedef uint16_t __u16; +typedef uint32_t __u32; +typedef uint64_t __u64; +typedef int32_t __s32; +#endif + +#ifndef __kernel_rwf_t +typedef int __kernel_rwf_t; +#endif + +typedef struct __SWIFT_IORING_SQE_FALLBACK_STRUCT swift_io_uring_sqe; + +struct io_uring_cqe { + __u64 user_data; + __s32 res; + __u32 flags; +}; + +struct io_sqring_offsets { + __u32 head; + __u32 tail; + __u32 ring_mask; + __u32 ring_entries; + __u32 flags; + __u32 dropped; + __u32 array; + __u32 resv1; + __u64 user_addr; +}; + +struct io_cqring_offsets { + __u32 head; + __u32 tail; + __u32 ring_mask; + __u32 ring_entries; + __u32 overflow; + __u32 cqes; + __u32 flags; + __u32 resv1; + __u64 user_addr; +}; + +struct io_uring_params { + __u32 sq_entries; + __u32 cq_entries; + __u32 flags; + __u32 sq_thread_cpu; + __u32 sq_thread_idle; + __u32 features; + __u32 wq_fd; + __u32 resv[3]; + struct io_sqring_offsets sq_off; + struct io_cqring_offsets cq_off; +}; +#endif // __has_include() + +#ifndef SWIFT_IORING_C_WRAPPER +#define SWIFT_IORING_C_WRAPPER + +# ifndef __NR_io_uring_setup +# define __NR_io_uring_setup 425 +# endif +# ifndef __NR_io_uring_enter +# define __NR_io_uring_enter 426 +# endif +# ifndef __NR_io_uring_register +# define __NR_io_uring_register 427 +# endif + +/* +struct io_uring_getevents_arg { + __u64 sigmask; + __u32 sigmask_sz; + __u32 min_wait_usec; //used to be called `pad`. This compatibility wrapper avoids dealing with that. + __u64 ts; +}; +*/ +struct swift_io_uring_getevents_arg { + __u64 sigmask; + __u32 sigmask_sz; + __u32 min_wait_usec; + __u64 ts; +}; + +static inline int io_uring_register(int fd, unsigned int opcode, void *arg, + unsigned int nr_args) +{ + return syscall(__NR_io_uring_register, fd, opcode, arg, nr_args); +} + +static inline int io_uring_setup(unsigned int entries, struct io_uring_params *p) +{ + return syscall(__NR_io_uring_setup, entries, p); +} + +static inline int io_uring_enter2(int fd, unsigned int to_submit, unsigned int min_complete, + unsigned int flags, void *args, size_t sz) +{ + return syscall(__NR_io_uring_enter, fd, to_submit, min_complete, + flags, args, _NSIG / 8); +} + +static inline int io_uring_enter(int fd, unsigned int to_submit, unsigned int min_complete, + unsigned int flags, sigset_t *sig) +{ + return io_uring_enter2(fd, to_submit, min_complete, flags, sig, _NSIG / 8); +} + +#endif diff --git a/Sources/CSystem/include/module.modulemap b/Sources/CSystem/include/module.modulemap new file mode 100644 index 00000000..6e8b89e9 --- /dev/null +++ b/Sources/CSystem/include/module.modulemap @@ -0,0 +1,6 @@ +module CSystem { + header "CSystemLinux.h" + header "CSystemWASI.h" + header "CSystemWindows.h" + export * +} diff --git a/Sources/System/CMakeLists.txt b/Sources/System/CMakeLists.txt new file mode 100644 index 00000000..1f3420a1 --- /dev/null +++ b/Sources/System/CMakeLists.txt @@ -0,0 +1,50 @@ +#[[ +This source file is part of the Swift System open source project + +Copyright (c) 2020 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 +#]] + +add_library(SystemPackage + Errno.swift + FileDescriptor.swift + FileHelpers.swift + FileOperations.swift + FilePermissions.swift + MachPort.swift + PlatformString.swift + SystemString.swift + Util.swift + Util+StringArray.swift + UtilConsumers.swift) +set_target_properties(SystemPackage PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) +target_sources(SystemPackage PRIVATE + FilePath/FilePath.swift + FilePath/FilePathComponents.swift + FilePath/FilePathComponentView.swift + FilePath/FilePathParsing.swift + FilePath/FilePathString.swift + FilePath/FilePathSyntax.swift + FilePath/FilePathWindows.swift) +target_sources(SystemPackage PRIVATE + Internals/Backcompat.swift + Internals/CInterop.swift + Internals/Constants.swift + Internals/Exports.swift + Internals/Mocking.swift + Internals/RawBuffer.swift + Internals/Syscalls.swift + Internals/WindowsSyscallAdapters.swift) +target_link_libraries(SystemPackage PUBLIC + CSystem) + +set(SWIFT_SYSTEM_APPLE_PLATFORMS "Darwin" "iOS" "watchOS" "tvOS" "visionOS") +if(CMAKE_SYSTEM_NAME IN_LIST SWIFT_SYSTEM_APPLE_PLATFORMS) + target_compile_definitions(SystemPackage PRIVATE SYSTEM_PACKAGE_DARWIN) +endif() + +_install_target(SystemPackage) +set_property(GLOBAL APPEND PROPERTY SWIFT_SYSTEM_EXPORTS SystemPackage) diff --git a/Sources/System/DarwinPlatformConstants.swift b/Sources/System/DarwinPlatformConstants.swift deleted file mode 100644 index 305fd356..00000000 --- a/Sources/System/DarwinPlatformConstants.swift +++ /dev/null @@ -1,427 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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(macOS) || os(iOS) || os(watchOS) || os(tvOS) - -// For platform constants redefined in Swift. We redefine them here so that they -// can be @_alwaysEmitIntoClient without depending on Darwin or re-including a -// header (and applying attributes). - -// MARK: errno -@_alwaysEmitIntoClient -internal var _ERRNO_NOT_USED: CInt { 0 } - -@_alwaysEmitIntoClient -internal var _EPERM: CInt { 1 } - -@_alwaysEmitIntoClient -internal var _ENOENT: CInt { 2 } - -@_alwaysEmitIntoClient -internal var _ESRCH: CInt { 3 } - -@_alwaysEmitIntoClient -internal var _EINTR: CInt { 4 } - -@_alwaysEmitIntoClient -internal var _EIO: CInt { 5 } - -@_alwaysEmitIntoClient -internal var _ENXIO: CInt { 6 } - -@_alwaysEmitIntoClient -internal var _E2BIG: CInt { 7 } - -@_alwaysEmitIntoClient -internal var _ENOEXEC: CInt { 8 } - -@_alwaysEmitIntoClient -internal var _EBADF: CInt { 9 } - -@_alwaysEmitIntoClient -internal var _ECHILD: CInt { 10 } - -@_alwaysEmitIntoClient -internal var _EDEADLK: CInt { 11 } - -@_alwaysEmitIntoClient -internal var _ENOMEM: CInt { 12 } - -@_alwaysEmitIntoClient -internal var _EACCES: CInt { 13 } - -@_alwaysEmitIntoClient -internal var _EFAULT: CInt { 14 } - -@_alwaysEmitIntoClient -internal var _ENOTBLK: CInt { 15 } - -@_alwaysEmitIntoClient -internal var _EBUSY: CInt { 16 } - -@_alwaysEmitIntoClient -internal var _EEXIST: CInt { 17 } - -@_alwaysEmitIntoClient -internal var _EXDEV: CInt { 18 } - -@_alwaysEmitIntoClient -internal var _ENODEV: CInt { 19 } - -@_alwaysEmitIntoClient -internal var _ENOTDIR: CInt { 20 } - -@_alwaysEmitIntoClient -internal var _EISDIR: CInt { 21 } - -@_alwaysEmitIntoClient -internal var _EINVAL: CInt { 22 } - -@_alwaysEmitIntoClient -internal var _ENFILE: CInt { 23 } - -@_alwaysEmitIntoClient -internal var _EMFILE: CInt { 24 } - -@_alwaysEmitIntoClient -internal var _ENOTTY: CInt { 25 } - -@_alwaysEmitIntoClient -internal var _ETXTBSY: CInt { 26 } - -@_alwaysEmitIntoClient -internal var _EFBIG: CInt { 27 } - -@_alwaysEmitIntoClient -internal var _ENOSPC: CInt { 28 } - -@_alwaysEmitIntoClient -internal var _ESPIPE: CInt { 29 } - -@_alwaysEmitIntoClient -internal var _EROFS: CInt { 30 } - -@_alwaysEmitIntoClient -internal var _EMLINK: CInt { 31 } - -@_alwaysEmitIntoClient -internal var _EPIPE: CInt { 32 } - -@_alwaysEmitIntoClient -internal var _EDOM: CInt { 33 } - -@_alwaysEmitIntoClient -internal var _ERANGE: CInt { 34 } - -@_alwaysEmitIntoClient -internal var _EAGAIN: CInt { 35 } - -@_alwaysEmitIntoClient -internal var _EWOULDBLOCK: CInt { _EAGAIN } - -@_alwaysEmitIntoClient -internal var _EINPROGRESS: CInt { 36 } - -@_alwaysEmitIntoClient -internal var _EALREADY: CInt { 37 } - -@_alwaysEmitIntoClient -internal var _ENOTSOCK: CInt { 38 } - -@_alwaysEmitIntoClient -internal var _EDESTADDRREQ: CInt { 39 } - -@_alwaysEmitIntoClient -internal var _EMSGSIZE: CInt { 40 } - -@_alwaysEmitIntoClient -internal var _EPROTOTYPE: CInt { 41 } - -@_alwaysEmitIntoClient -internal var _ENOPROTOOPT: CInt { 42 } - -@_alwaysEmitIntoClient -internal var _EPROTONOSUPPORT: CInt { 43 } - -@_alwaysEmitIntoClient -internal var _ESOCKTNOSUPPORT: CInt { 44 } - -@_alwaysEmitIntoClient -internal var _ENOTSUP: CInt { 45 } - -@_alwaysEmitIntoClient -internal var _EPFNOSUPPORT: CInt { 46 } - -@_alwaysEmitIntoClient -internal var _EAFNOSUPPORT: CInt { 47 } - -@_alwaysEmitIntoClient -internal var _EADDRINUSE: CInt { 48 } - -@_alwaysEmitIntoClient -internal var _EADDRNOTAVAIL: CInt { 49 } - -@_alwaysEmitIntoClient -internal var _ENETDOWN: CInt { 50 } - -@_alwaysEmitIntoClient -internal var _ENETUNREACH: CInt { 51 } - -@_alwaysEmitIntoClient -internal var _ENETRESET: CInt { 52 } - -@_alwaysEmitIntoClient -internal var _ECONNABORTED: CInt { 53 } - -@_alwaysEmitIntoClient -internal var _ECONNRESET: CInt { 54 } - -@_alwaysEmitIntoClient -internal var _ENOBUFS: CInt { 55 } - -@_alwaysEmitIntoClient -internal var _EISCONN: CInt { 56 } - -@_alwaysEmitIntoClient -internal var _ENOTCONN: CInt { 57 } - -@_alwaysEmitIntoClient -internal var _ESHUTDOWN: CInt { 58 } - -@_alwaysEmitIntoClient -internal var _ETOOMANYREFS: CInt { 59 } - -@_alwaysEmitIntoClient -internal var _ETIMEDOUT: CInt { 60 } - -@_alwaysEmitIntoClient -internal var _ECONNREFUSED: CInt { 61 } - -@_alwaysEmitIntoClient -internal var _ELOOP: CInt { 62 } - -@_alwaysEmitIntoClient -internal var _ENAMETOOLONG: CInt { 63 } - -@_alwaysEmitIntoClient -internal var _EHOSTDOWN: CInt { 64 } - -@_alwaysEmitIntoClient -internal var _EHOSTUNREACH: CInt { 65 } - -@_alwaysEmitIntoClient -internal var _ENOTEMPTY: CInt { 66 } - -@_alwaysEmitIntoClient -internal var _EPROCLIM: CInt { 67 } - -@_alwaysEmitIntoClient -internal var _EUSERS: CInt { 68 } - -@_alwaysEmitIntoClient -internal var _EDQUOT: CInt { 69 } - -@_alwaysEmitIntoClient -internal var _ESTALE: CInt { 70 } - -@_alwaysEmitIntoClient -internal var _EREMOTE: CInt { 71 } - -@_alwaysEmitIntoClient -internal var _EBADRPC: CInt { 72 } - -@_alwaysEmitIntoClient -internal var _ERPCMISMATCH: CInt { 73 } - -@_alwaysEmitIntoClient -internal var _EPROGUNAVAIL: CInt { 74 } - -@_alwaysEmitIntoClient -internal var _EPROGMISMATCH: CInt { 75 } - -@_alwaysEmitIntoClient -internal var _EPROCUNAVAIL: CInt { 76 } - -@_alwaysEmitIntoClient -internal var _ENOLCK: CInt { 77 } - -@_alwaysEmitIntoClient -internal var _ENOSYS: CInt { 78 } - -@_alwaysEmitIntoClient -internal var _EFTYPE: CInt { 79 } - -@_alwaysEmitIntoClient -internal var _EAUTH: CInt { 80 } - -@_alwaysEmitIntoClient -internal var _ENEEDAUTH: CInt { 81 } - -@_alwaysEmitIntoClient -internal var _EPWROFF: CInt { 82 } - -@_alwaysEmitIntoClient -internal var _EDEVERR: CInt { 83 } - -@_alwaysEmitIntoClient -internal var _EOVERFLOW: CInt { 84 } - -@_alwaysEmitIntoClient -internal var _EBADEXEC: CInt { 85 } - -@_alwaysEmitIntoClient -internal var _EBADARCH: CInt { 86 } - -@_alwaysEmitIntoClient -internal var _ESHLIBVERS: CInt { 87 } - -@_alwaysEmitIntoClient -internal var _EBADMACHO: CInt { 88 } - -@_alwaysEmitIntoClient -internal var _ECANCELED: CInt { 89 } - -@_alwaysEmitIntoClient -internal var _EIDRM: CInt { 90 } - -@_alwaysEmitIntoClient -internal var _ENOMSG: CInt { 91 } - -@_alwaysEmitIntoClient -internal var _EILSEQ: CInt { 92 } - -@_alwaysEmitIntoClient -internal var _ENOATTR: CInt { 93 } - -@_alwaysEmitIntoClient -internal var _EBADMSG: CInt { 94 } - -@_alwaysEmitIntoClient -internal var _EMULTIHOP: CInt { 95 } - -@_alwaysEmitIntoClient -internal var _ENODATA: CInt { 96 } - -@_alwaysEmitIntoClient -internal var _ENOLINK: CInt { 97 } - -@_alwaysEmitIntoClient -internal var _ENOSR: CInt { 98 } - -@_alwaysEmitIntoClient -internal var _ENOSTR: CInt { 99 } - -@_alwaysEmitIntoClient -internal var _EPROTO: CInt { 100 } - -@_alwaysEmitIntoClient -internal var _ETIME: CInt { 101 } - -@_alwaysEmitIntoClient -internal var _EOPNOTSUPP: CInt { 102 } - -@_alwaysEmitIntoClient -internal var _ENOPOLICY: CInt { 103 } - -@_alwaysEmitIntoClient -internal var _ENOTRECOVERABLE: CInt { 104 } - -@_alwaysEmitIntoClient -internal var _EOWNERDEAD: CInt { 105 } - -@_alwaysEmitIntoClient -internal var _EQFULL: CInt { 106 } - -@_alwaysEmitIntoClient -internal var _ELAST: CInt { 106 } - -// MARK: File Operations - -@_alwaysEmitIntoClient -internal var _O_RDONLY: CInt { 0x0000 } - -@_alwaysEmitIntoClient -internal var _O_WRONLY: CInt { 0x0001 } - -@_alwaysEmitIntoClient -internal var _O_RDWR: CInt { 0x0002 } - -// TODO: API? -@_alwaysEmitIntoClient -internal var _O_ACCMODE: CInt { 0x0003 } - -@_alwaysEmitIntoClient -internal var _O_NONBLOCK: CInt { 0x0004 } - -@_alwaysEmitIntoClient -internal var _O_APPEND: CInt { 0x0008 } - -@_alwaysEmitIntoClient -internal var _O_SHLOCK: CInt { 0x0010 } - -@_alwaysEmitIntoClient -internal var _O_EXLOCK: CInt { 0x0020 } - -// TODO: API? -@_alwaysEmitIntoClient -internal var _O_ASYNC: CInt { 0x0040 } - -@_alwaysEmitIntoClient -internal var _O_NOFOLLOW: CInt { 0x0100 } - -@_alwaysEmitIntoClient -internal var _O_CREAT: CInt { 0x0200 } - -@_alwaysEmitIntoClient -internal var _O_TRUNC: CInt { 0x0400 } - -@_alwaysEmitIntoClient -internal var _O_EXCL: CInt { 0x0800 } - -@_alwaysEmitIntoClient -internal var _O_EVTONLY: CInt { 0x8000 } - -// TODO: API? -@_alwaysEmitIntoClient -internal var _O_NOCTTY: CInt { 0x20000 } - -// TODO: API? -@_alwaysEmitIntoClient -internal var _O_DIRECTORY: CInt { 0x100000 } - -@_alwaysEmitIntoClient -internal var _O_SYMLINK: CInt { 0x200000 } - -@_alwaysEmitIntoClient -internal var _O_CLOEXEC: CInt { 0x1000000 } - -// TODO: API? -@_alwaysEmitIntoClient -internal var _O_DP_GETRAWENCRYPTED: CInt { 0x0001 } - -// TODO: API? -@_alwaysEmitIntoClient -internal var _O_DP_GETRAWUNENCRYPTED: CInt { 0x0002 } - -@_alwaysEmitIntoClient -internal var _SEEK_SET: CInt { 0 } - -@_alwaysEmitIntoClient -internal var _SEEK_CUR: CInt { 1 } - -@_alwaysEmitIntoClient -internal var _SEEK_END: CInt { 2 } - -@_alwaysEmitIntoClient -internal var _SEEK_HOLE: CInt { 3 } - -@_alwaysEmitIntoClient -internal var _SEEK_DATA: CInt { 4 } - -#endif diff --git a/Sources/System/Errno.swift b/Sources/System/Errno.swift index aeddbc31..94eb102d 100644 --- a/Sources/System/Errno.swift +++ b/Sources/System/Errno.swift @@ -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) 2021 - 2024 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 @@ -10,7 +10,7 @@ /// An error number used by system calls to communicate what kind of error /// occurred. @frozen -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) public struct Errno: RawRepresentable, Error, Hashable, Codable { /// The raw C error number. @_alwaysEmitIntoClient @@ -20,13 +20,10 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { @_alwaysEmitIntoClient public init(rawValue: CInt) { self.rawValue = rawValue } - @_alwaysEmitIntoClient - private init(_ raw: CInt) { self.init(rawValue: raw) } - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN /// Error. Not used. @_alwaysEmitIntoClient - public static var notUsed: Errno { Errno(_ERRNO_NOT_USED) } + public static var notUsed: Errno { .init(rawValue: _ERRNO_NOT_USED) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "notUsed") @@ -41,7 +38,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EPERM`. @_alwaysEmitIntoClient - public static var notPermitted: Errno { Errno(_EPERM) } + public static var notPermitted: Errno { .init(rawValue: _EPERM) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "notPermitted") @@ -54,7 +51,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOENT`. @_alwaysEmitIntoClient - public static var noSuchFileOrDirectory: Errno { Errno(_ENOENT) } + public static var noSuchFileOrDirectory: Errno { .init(rawValue: _ENOENT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noSuchFileOrDirectory") @@ -66,7 +63,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ESRCH`. @_alwaysEmitIntoClient - public static var noSuchProcess: Errno { Errno(_ESRCH) } + public static var noSuchProcess: Errno { .init(rawValue: _ESRCH) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noSuchProcess") @@ -81,7 +78,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EINTR`. @_alwaysEmitIntoClient - public static var interrupted: Errno { Errno(_EINTR) } + public static var interrupted: Errno { .init(rawValue: _EINTR) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "interrupted") @@ -96,7 +93,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EIO`. @_alwaysEmitIntoClient - public static var ioError: Errno { Errno(_EIO) } + public static var ioError: Errno { .init(rawValue: _EIO) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "ioError") @@ -111,7 +108,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENXIO`. @_alwaysEmitIntoClient - public static var noSuchAddressOrDevice: Errno { Errno(_ENXIO) } + public static var noSuchAddressOrDevice: Errno { .init(rawValue: _ENXIO) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noSuchAddressOrDevice") @@ -125,7 +122,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `E2BIG`. @_alwaysEmitIntoClient - public static var argListTooLong: Errno { Errno(_E2BIG) } + public static var argListTooLong: Errno { .init(rawValue: _E2BIG) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "argListTooLong") @@ -139,10 +136,10 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOEXEC`. @_alwaysEmitIntoClient - public static var execFormatError: Errno { Errno(_ENOEXEC) } + public static var execFormatError: Errno { .init(rawValue: _ENOEXEC) } @_alwaysEmitIntoClient - @available(*, unavailable, renamed: "noExec") + @available(*, unavailable, renamed: "execFormatError") public static var ENOEXEC: Errno { execFormatError } /// Bad file descriptor. @@ -154,7 +151,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EBADF`. @_alwaysEmitIntoClient - public static var badFileDescriptor: Errno { Errno(_EBADF) } + public static var badFileDescriptor: Errno { .init(rawValue: _EBADF) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "badFileDescriptor") @@ -168,7 +165,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ECHILD`. @_alwaysEmitIntoClient - public static var noChildProcess: Errno { Errno(_ECHILD) } + public static var noChildProcess: Errno { .init(rawValue: _ECHILD) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noChildProcess") @@ -181,7 +178,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EDEADLK`. @_alwaysEmitIntoClient - public static var deadlock: Errno { Errno(_EDEADLK) } + public static var deadlock: Errno { .init(rawValue: _EDEADLK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "deadlock") @@ -198,7 +195,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOMEM`. @_alwaysEmitIntoClient - public static var noMemory: Errno { Errno(_ENOMEM) } + public static var noMemory: Errno { .init(rawValue: _ENOMEM) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noMemory") @@ -211,7 +208,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EACCES`. @_alwaysEmitIntoClient - public static var permissionDenied: Errno { Errno(_EACCES) } + public static var permissionDenied: Errno { .init(rawValue: _EACCES) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "permissionDenied") @@ -223,20 +220,20 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EFAULT`. @_alwaysEmitIntoClient - public static var badAddress: Errno { Errno(_EFAULT) } + public static var badAddress: Errno { .init(rawValue: _EFAULT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "badAddress") public static var EFAULT: Errno { badAddress } -#if !os(Windows) +#if !os(Windows) && !os(WASI) /// Not a block device. /// /// You attempted a block device operation on a nonblock device or file. /// /// The corresponding C error is `ENOTBLK`. @_alwaysEmitIntoClient - public static var notBlockDevice: Errno { Errno(_ENOTBLK) } + public static var notBlockDevice: Errno { .init(rawValue: _ENOTBLK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "notBlockDevice") @@ -250,7 +247,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EBUSY`. @_alwaysEmitIntoClient - public static var resourceBusy: Errno { Errno(_EBUSY) } + public static var resourceBusy: Errno { .init(rawValue: _EBUSY) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "resourceBusy") @@ -263,7 +260,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EEXIST`. @_alwaysEmitIntoClient - public static var fileExists: Errno { Errno(_EEXIST) } + public static var fileExists: Errno { .init(rawValue: _EEXIST) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "fileExists") @@ -275,7 +272,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EXDEV`. @_alwaysEmitIntoClient - public static var improperLink: Errno { Errno(_EXDEV) } + public static var improperLink: Errno { .init(rawValue: _EXDEV) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "improperLink") @@ -288,7 +285,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENODEV`. @_alwaysEmitIntoClient - public static var operationNotSupportedByDevice: Errno { Errno(_ENODEV) } + public static var operationNotSupportedByDevice: Errno { .init(rawValue: _ENODEV) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "operationNotSupportedByDevice") @@ -302,7 +299,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOTDIR`. @_alwaysEmitIntoClient - public static var notDirectory: Errno { Errno(_ENOTDIR) } + public static var notDirectory: Errno { .init(rawValue: _ENOTDIR) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "notDirectory") @@ -315,7 +312,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EISDIR`. @_alwaysEmitIntoClient - public static var isDirectory: Errno { Errno(_EISDIR) } + public static var isDirectory: Errno { .init(rawValue: _EISDIR) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "isDirectory") @@ -328,7 +325,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EINVAL`. @_alwaysEmitIntoClient - public static var invalidArgument: Errno { Errno(_EINVAL) } + public static var invalidArgument: Errno { .init(rawValue: _EINVAL) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "invalidArgument") @@ -343,7 +340,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENFILE`. @_alwaysEmitIntoClient - public static var tooManyOpenFilesInSystem: Errno { Errno(_ENFILE) } + public static var tooManyOpenFilesInSystem: Errno { .init(rawValue: _ENFILE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "tooManyOpenFilesInSystem") @@ -356,7 +353,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EMFILE`. @_alwaysEmitIntoClient - public static var tooManyOpenFiles: Errno { Errno(_EMFILE) } + public static var tooManyOpenFiles: Errno { .init(rawValue: _EMFILE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "tooManyOpenFiles") @@ -371,7 +368,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOTTY`. @_alwaysEmitIntoClient - public static var inappropriateIOCTLForDevice: Errno { Errno(_ENOTTY) } + public static var inappropriateIOCTLForDevice: Errno { .init(rawValue: _ENOTTY) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "inappropriateIOCTLForDevice") @@ -386,7 +383,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ETXTBSY`. @_alwaysEmitIntoClient - public static var textFileBusy: Errno { Errno(_ETXTBSY) } + public static var textFileBusy: Errno { .init(rawValue: _ETXTBSY) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "textFileBusy") @@ -401,7 +398,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EFBIG`. @_alwaysEmitIntoClient - public static var fileTooLarge: Errno { Errno(_EFBIG) } + public static var fileTooLarge: Errno { .init(rawValue: _EFBIG) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "fileTooLarge") @@ -418,7 +415,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOSPC`. @_alwaysEmitIntoClient - public static var noSpace: Errno { Errno(_ENOSPC) } + public static var noSpace: Errno { .init(rawValue: _ENOSPC) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noSpace") @@ -430,7 +427,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ESPIPE`. @_alwaysEmitIntoClient - public static var illegalSeek: Errno { Errno(_ESPIPE) } + public static var illegalSeek: Errno { .init(rawValue: _ESPIPE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "illegalSeek") @@ -443,7 +440,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EROFS`. @_alwaysEmitIntoClient - public static var readOnlyFileSystem: Errno { Errno(_EROFS) } + public static var readOnlyFileSystem: Errno { .init(rawValue: _EROFS) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "readOnlyFileSystem") @@ -456,7 +453,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EMLINK`. @_alwaysEmitIntoClient - public static var tooManyLinks: Errno { Errno(_EMLINK) } + public static var tooManyLinks: Errno { .init(rawValue: _EMLINK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "tooManyLinks") @@ -469,7 +466,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EPIPE`. @_alwaysEmitIntoClient - public static var brokenPipe: Errno { Errno(_EPIPE) } + public static var brokenPipe: Errno { .init(rawValue: _EPIPE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "brokenPipe") @@ -482,7 +479,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EDOM`. @_alwaysEmitIntoClient - public static var outOfDomain: Errno { Errno(_EDOM) } + public static var outOfDomain: Errno { .init(rawValue: _EDOM) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "outOfDomain") @@ -497,7 +494,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ERANGE`. @_alwaysEmitIntoClient - public static var outOfRange: Errno { Errno(_ERANGE) } + public static var outOfRange: Errno { .init(rawValue: _ERANGE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "outOfRange") @@ -511,7 +508,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EAGAIN`. @_alwaysEmitIntoClient - public static var resourceTemporarilyUnavailable: Errno { Errno(_EAGAIN) } + public static var resourceTemporarilyUnavailable: Errno { .init(rawValue: _EAGAIN) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "resourceTemporarilyUnavailable") @@ -526,10 +523,10 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EINPROGRESS`. @_alwaysEmitIntoClient - public static var nowInProgress: Errno { Errno(_EINPROGRESS) } + public static var nowInProgress: Errno { .init(rawValue: _EINPROGRESS) } @_alwaysEmitIntoClient - @available(*, unavailable, renamed: "nowInProcess") + @available(*, unavailable, renamed: "nowInProgress") public static var EINPROGRESS: Errno { nowInProgress } /// Operation already in progress. @@ -539,7 +536,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EALREADY`. @_alwaysEmitIntoClient - public static var alreadyInProcess: Errno { Errno(_EALREADY) } + public static var alreadyInProcess: Errno { .init(rawValue: _EALREADY) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "alreadyInProcess") @@ -549,7 +546,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOTSOCK`. @_alwaysEmitIntoClient - public static var notSocket: Errno { Errno(_ENOTSOCK) } + public static var notSocket: Errno { .init(rawValue: _ENOTSOCK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "notSocket") @@ -561,7 +558,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EDESTADDRREQ`. @_alwaysEmitIntoClient - public static var addressRequired: Errno { Errno(_EDESTADDRREQ) } + public static var addressRequired: Errno { .init(rawValue: _EDESTADDRREQ) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "addressRequired") @@ -574,7 +571,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EMSGSIZE`. @_alwaysEmitIntoClient - public static var messageTooLong: Errno { Errno(_EMSGSIZE) } + public static var messageTooLong: Errno { .init(rawValue: _EMSGSIZE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "messageTooLong") @@ -589,7 +586,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EPROTOTYPE`. @_alwaysEmitIntoClient - public static var protocolWrongTypeForSocket: Errno { Errno(_EPROTOTYPE) } + public static var protocolWrongTypeForSocket: Errno { .init(rawValue: _EPROTOTYPE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "protocolWrongTypeForSocket") @@ -602,7 +599,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOPROTOOPT`. @_alwaysEmitIntoClient - public static var protocolNotAvailable: Errno { Errno(_ENOPROTOOPT) } + public static var protocolNotAvailable: Errno { .init(rawValue: _ENOPROTOOPT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "protocolNotAvailable") @@ -615,12 +612,13 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EPROTONOSUPPORT`. @_alwaysEmitIntoClient - public static var protocolNotSupported: Errno { Errno(_EPROTONOSUPPORT) } + public static var protocolNotSupported: Errno { .init(rawValue: _EPROTONOSUPPORT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "protocolNotSupported") public static var EPROTONOSUPPORT: Errno { protocolNotSupported } +#if !os(WASI) /// Socket type not supported. /// /// Support for the socket type hasn't been configured into the system @@ -628,13 +626,13 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ESOCKTNOSUPPORT`. @_alwaysEmitIntoClient - public static var socketTypeNotSupported: Errno { Errno(_ESOCKTNOSUPPORT) } + public static var socketTypeNotSupported: Errno { .init(rawValue: _ESOCKTNOSUPPORT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "socketTypeNotSupported") public static var ESOCKTNOSUPPORT: Errno { socketTypeNotSupported } +#endif -#if !os(Windows) /// Not supported. /// /// The attempted operation isn't supported @@ -642,13 +640,13 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOTSUP`. @_alwaysEmitIntoClient - public static var notSupported: Errno { Errno(_ENOTSUP) } + public static var notSupported: Errno { .init(rawValue: _ENOTSUP) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "notSupported") public static var ENOTSUP: Errno { notSupported } -#endif +#if !os(WASI) /// Protocol family not supported. /// /// The protocol family hasn't been configured into the system @@ -656,11 +654,12 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EPFNOSUPPORT`. @_alwaysEmitIntoClient - public static var protocolFamilyNotSupported: Errno { Errno(_EPFNOSUPPORT) } + public static var protocolFamilyNotSupported: Errno { .init(rawValue: _EPFNOSUPPORT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "protocolFamilyNotSupported") public static var EPFNOSUPPORT: Errno { protocolFamilyNotSupported } +#endif /// The address family isn't supported by the protocol family. /// @@ -670,7 +669,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EAFNOSUPPORT`. @_alwaysEmitIntoClient - public static var addressFamilyNotSupported: Errno { Errno(_EAFNOSUPPORT) } + public static var addressFamilyNotSupported: Errno { .init(rawValue: _EAFNOSUPPORT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "addressFamilyNotSupported") @@ -682,7 +681,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EADDRINUSE`. @_alwaysEmitIntoClient - public static var addressInUse: Errno { Errno(_EADDRINUSE) } + public static var addressInUse: Errno { .init(rawValue: _EADDRINUSE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "addressInUse") @@ -695,7 +694,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EADDRNOTAVAIL`. @_alwaysEmitIntoClient - public static var addressNotAvailable: Errno { Errno(_EADDRNOTAVAIL) } + public static var addressNotAvailable: Errno { .init(rawValue: _EADDRNOTAVAIL) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "addressNotAvailable") @@ -707,7 +706,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENETDOWN`. @_alwaysEmitIntoClient - public static var networkDown: Errno { Errno(_ENETDOWN) } + public static var networkDown: Errno { .init(rawValue: _ENETDOWN) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "networkDown") @@ -719,7 +718,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENETUNREACH`. @_alwaysEmitIntoClient - public static var networkUnreachable: Errno { Errno(_ENETUNREACH) } + public static var networkUnreachable: Errno { .init(rawValue: _ENETUNREACH) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "networkUnreachable") @@ -731,7 +730,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENETRESET`. @_alwaysEmitIntoClient - public static var networkReset: Errno { Errno(_ENETRESET) } + public static var networkReset: Errno { .init(rawValue: _ENETRESET) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "networkReset") @@ -743,7 +742,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ECONNABORTED`. @_alwaysEmitIntoClient - public static var connectionAbort: Errno { Errno(_ECONNABORTED) } + public static var connectionAbort: Errno { .init(rawValue: _ECONNABORTED) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "connectionAbort") @@ -757,7 +756,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ECONNRESET`. @_alwaysEmitIntoClient - public static var connectionReset: Errno { Errno(_ECONNRESET) } + public static var connectionReset: Errno { .init(rawValue: _ECONNRESET) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "connectionReset") @@ -771,7 +770,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOBUFS`. @_alwaysEmitIntoClient - public static var noBufferSpace: Errno { Errno(_ENOBUFS) } + public static var noBufferSpace: Errno { .init(rawValue: _ENOBUFS) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noBufferSpace") @@ -786,7 +785,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EISCONN`. @_alwaysEmitIntoClient - public static var socketIsConnected: Errno { Errno(_EISCONN) } + public static var socketIsConnected: Errno { .init(rawValue: _EISCONN) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "socketIsConnected") @@ -801,12 +800,13 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOTCONN`. @_alwaysEmitIntoClient - public static var socketNotConnected: Errno { Errno(_ENOTCONN) } + public static var socketNotConnected: Errno { .init(rawValue: _ENOTCONN) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "socketNotConnected") public static var ENOTCONN: Errno { socketNotConnected } +#if !os(WASI) /// Can't send after socket shutdown. /// /// A request to send data wasn't permitted @@ -815,11 +815,12 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ESHUTDOWN`. @_alwaysEmitIntoClient - public static var socketShutdown: Errno { Errno(_ESHUTDOWN) } + public static var socketShutdown: Errno { .init(rawValue: _ESHUTDOWN) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "socketShutdown") public static var ESHUTDOWN: Errno { socketShutdown } +#endif /// Operation timed out. /// @@ -830,7 +831,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ETIMEDOUT`. @_alwaysEmitIntoClient - public static var timedOut: Errno { Errno(_ETIMEDOUT) } + public static var timedOut: Errno { .init(rawValue: _ETIMEDOUT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "timedOut") @@ -845,7 +846,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ECONNREFUSED`. @_alwaysEmitIntoClient - public static var connectionRefused: Errno { Errno(_ECONNREFUSED) } + public static var connectionRefused: Errno { .init(rawValue: _ECONNREFUSED) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "connectionRefused") @@ -857,7 +858,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ELOOP`. @_alwaysEmitIntoClient - public static var tooManySymbolicLinkLevels: Errno { Errno(_ELOOP) } + public static var tooManySymbolicLinkLevels: Errno { .init(rawValue: _ELOOP) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "tooManySymbolicLinkLevels") @@ -870,23 +871,25 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENAMETOOLONG`. @_alwaysEmitIntoClient - public static var fileNameTooLong: Errno { Errno(_ENAMETOOLONG) } + public static var fileNameTooLong: Errno { .init(rawValue: _ENAMETOOLONG) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "fileNameTooLong") public static var ENAMETOOLONG: Errno { fileNameTooLong } +#if !os(WASI) /// The host is down. /// /// A socket operation failed because the destination host was down. /// /// The corresponding C error is `EHOSTDOWN`. @_alwaysEmitIntoClient - public static var hostIsDown: Errno { Errno(_EHOSTDOWN) } + public static var hostIsDown: Errno { .init(rawValue: _EHOSTDOWN) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "hostIsDown") public static var EHOSTDOWN: Errno { hostIsDown } +#endif /// No route to host. /// @@ -894,7 +897,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EHOSTUNREACH`. @_alwaysEmitIntoClient - public static var noRouteToHost: Errno { Errno(_EHOSTUNREACH) } + public static var noRouteToHost: Errno { .init(rawValue: _EHOSTUNREACH) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noRouteToHost") @@ -907,35 +910,37 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOTEMPTY`. @_alwaysEmitIntoClient - public static var directoryNotEmpty: Errno { Errno(_ENOTEMPTY) } + public static var directoryNotEmpty: Errno { .init(rawValue: _ENOTEMPTY) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "directoryNotEmpty") public static var ENOTEMPTY: Errno { directoryNotEmpty } -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN /// Too many processes. /// /// The corresponding C error is `EPROCLIM`. @_alwaysEmitIntoClient - public static var tooManyProcesses: Errno { Errno(_EPROCLIM) } + public static var tooManyProcesses: Errno { .init(rawValue: _EPROCLIM) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "tooManyProcesses") public static var EPROCLIM: Errno { tooManyProcesses } #endif +#if !os(WASI) /// Too many users. /// /// The quota system ran out of table entries. /// /// The corresponding C error is `EUSERS`. @_alwaysEmitIntoClient - public static var tooManyUsers: Errno { Errno(_EUSERS) } + public static var tooManyUsers: Errno { .init(rawValue: _EUSERS) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "tooManyUsers") public static var EUSERS: Errno { tooManyUsers } +#endif /// Disk quota exceeded. /// @@ -948,7 +953,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EDQUOT`. @_alwaysEmitIntoClient - public static var diskQuotaExceeded: Errno { Errno(_EDQUOT) } + public static var diskQuotaExceeded: Errno { .init(rawValue: _EDQUOT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "diskQuotaExceeded") @@ -956,21 +961,21 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// Stale NFS file handle. /// - /// You attempted access an open file on an NFS filesystem, + /// You attempted access an open file on an NFS file system, /// which is now unavailable as referenced by the given file descriptor. /// This may indicate that the file was deleted on the NFS server /// or that some other catastrophic event occurred. /// /// The corresponding C error is `ESTALE`. @_alwaysEmitIntoClient - public static var staleNFSFileHandle: Errno { Errno(_ESTALE) } + public static var staleNFSFileHandle: Errno { .init(rawValue: _ESTALE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "staleNFSFileHandle") public static var ESTALE: Errno { staleNFSFileHandle } // TODO: Add Linux's RPC equivalents -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN /// The structure of the remote procedure call (RPC) is bad. /// @@ -978,7 +983,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EBADRPC`. @_alwaysEmitIntoClient - public static var rpcUnsuccessful: Errno { Errno(_EBADRPC) } + public static var rpcUnsuccessful: Errno { .init(rawValue: _EBADRPC) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "rpcUnsuccessful") @@ -991,7 +996,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ERPCMISMATCH`. @_alwaysEmitIntoClient - public static var rpcVersionMismatch: Errno { Errno(_ERPCMISMATCH) } + public static var rpcVersionMismatch: Errno { .init(rawValue: _ERPCMISMATCH) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "rpcVersionMismatch") @@ -1003,7 +1008,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EPROGUNAVAIL`. @_alwaysEmitIntoClient - public static var rpcProgramUnavailable: Errno { Errno(_EPROGUNAVAIL) } + public static var rpcProgramUnavailable: Errno { .init(rawValue: _EPROGUNAVAIL) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "rpcProgramUnavailable") @@ -1016,7 +1021,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EPROGMISMATCH`. @_alwaysEmitIntoClient - public static var rpcProgramVersionMismatch: Errno { Errno(_EPROGMISMATCH) } + public static var rpcProgramVersionMismatch: Errno { .init(rawValue: _EPROGMISMATCH) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "rpcProgramVersionMismatch") @@ -1029,7 +1034,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EPROCUNAVAIL`. @_alwaysEmitIntoClient - public static var rpcProcedureUnavailable: Errno { Errno(_EPROCUNAVAIL) } + public static var rpcProcedureUnavailable: Errno { .init(rawValue: _EPROCUNAVAIL) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "rpcProcedureUnavailable") @@ -1043,7 +1048,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOLCK`. @_alwaysEmitIntoClient - public static var noLocks: Errno { Errno(_ENOLCK) } + public static var noLocks: Errno { .init(rawValue: _ENOLCK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noLocks") @@ -1055,14 +1060,14 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOSYS`. @_alwaysEmitIntoClient - public static var noFunction: Errno { Errno(_ENOSYS) } + public static var noFunction: Errno { .init(rawValue: _ENOSYS) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noFunction") public static var ENOSYS: Errno { noFunction } // BSD -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Inappropriate file type or format. /// /// The file was the wrong type for the operation, @@ -1070,21 +1075,21 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EFTYPE`. @_alwaysEmitIntoClient - public static var badFileTypeOrFormat: Errno { Errno(_EFTYPE) } + public static var badFileTypeOrFormat: Errno { .init(rawValue: _EFTYPE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "badFileTypeOrFormat") public static var EFTYPE: Errno { badFileTypeOrFormat } #endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Authentication error. /// /// The authentication ticket used to mount an NFS file system was invalid. /// /// The corresponding C error is `EAUTH`. @_alwaysEmitIntoClient - public static var authenticationError: Errno { Errno(_EAUTH) } + public static var authenticationError: Errno { .init(rawValue: _EAUTH) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "authenticationError") @@ -1097,19 +1102,19 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENEEDAUTH`. @_alwaysEmitIntoClient - public static var needAuthenticator: Errno { Errno(_ENEEDAUTH) } + public static var needAuthenticator: Errno { .init(rawValue: _ENEEDAUTH) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "needAuthenticator") public static var ENEEDAUTH: Errno { needAuthenticator } #endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN /// Device power is off. /// /// The corresponding C error is `EPWROFF`. @_alwaysEmitIntoClient - public static var devicePowerIsOff: Errno { Errno(_EPWROFF) } + public static var devicePowerIsOff: Errno { .init(rawValue: _EPWROFF) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "devicePowerIsOff") @@ -1122,7 +1127,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EDEVERR`. @_alwaysEmitIntoClient - public static var deviceError: Errno { Errno(_EDEVERR) } + public static var deviceError: Errno { .init(rawValue: _EDEVERR) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "deviceError") @@ -1137,21 +1142,21 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EOVERFLOW`. @_alwaysEmitIntoClient - public static var overflow: Errno { Errno(_EOVERFLOW) } + public static var overflow: Errno { .init(rawValue: _EOVERFLOW) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "overflow") public static var EOVERFLOW: Errno { overflow } #endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN /// Bad executable or shared library. /// /// The executable or shared library being referenced was malformed. /// /// The corresponding C error is `EBADEXEC`. @_alwaysEmitIntoClient - public static var badExecutable: Errno { Errno(_EBADEXEC) } + public static var badExecutable: Errno { .init(rawValue: _EBADEXEC) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "badExecutable") @@ -1163,7 +1168,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EBADARCH`. @_alwaysEmitIntoClient - public static var badCPUType: Errno { Errno(_EBADARCH) } + public static var badCPUType: Errno { .init(rawValue: _EBADARCH) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "badCPUType") @@ -1176,7 +1181,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ESHLIBVERS`. @_alwaysEmitIntoClient - public static var sharedLibraryVersionMismatch: Errno { Errno(_ESHLIBVERS) } + public static var sharedLibraryVersionMismatch: Errno { .init(rawValue: _ESHLIBVERS) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "sharedLibraryVersionMismatch") @@ -1188,7 +1193,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EBADMACHO`. @_alwaysEmitIntoClient - public static var malformedMachO: Errno { Errno(_EBADMACHO) } + public static var malformedMachO: Errno { .init(rawValue: _EBADMACHO) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "malformedMachO") @@ -1201,7 +1206,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ECANCELED`. @_alwaysEmitIntoClient - public static var canceled: Errno { Errno(_ECANCELED) } + public static var canceled: Errno { .init(rawValue: _ECANCELED) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "canceled") @@ -1214,7 +1219,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EIDRM`. @_alwaysEmitIntoClient - public static var identifierRemoved: Errno { Errno(_EIDRM) } + public static var identifierRemoved: Errno { .init(rawValue: _EIDRM) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "identifierRemoved") @@ -1227,7 +1232,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOMSG`. @_alwaysEmitIntoClient - public static var noMessage: Errno { Errno(_ENOMSG) } + public static var noMessage: Errno { .init(rawValue: _ENOMSG) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noMessage") @@ -1242,20 +1247,20 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EILSEQ`. @_alwaysEmitIntoClient - public static var illegalByteSequence: Errno { Errno(_EILSEQ) } + public static var illegalByteSequence: Errno { .init(rawValue: _EILSEQ) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "illegalByteSequence") public static var EILSEQ: Errno { illegalByteSequence } -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Attribute not found. /// /// The specified extended attribute doesn't exist. /// /// The corresponding C error is `ENOATTR`. @_alwaysEmitIntoClient - public static var attributeNotFound: Errno { Errno(_ENOATTR) } + public static var attributeNotFound: Errno { .init(rawValue: _ENOATTR) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "attributeNotFound") @@ -1270,35 +1275,38 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EBADMSG`. @_alwaysEmitIntoClient - public static var badMessage: Errno { Errno(_EBADMSG) } + public static var badMessage: Errno { .init(rawValue: _EBADMSG) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "badMessage") public static var EBADMSG: Errno { badMessage } +#if !os(OpenBSD) /// Reserved. /// /// This error is reserved for future use. /// /// The corresponding C error is `EMULTIHOP`. @_alwaysEmitIntoClient - public static var multiHop: Errno { Errno(_EMULTIHOP) } + public static var multiHop: Errno { .init(rawValue: _EMULTIHOP) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "multiHop") public static var EMULTIHOP: Errno { multiHop } +#if !os(WASI) && !os(FreeBSD) /// No message available. /// /// No message was available to be received by the requested operation. /// /// The corresponding C error is `ENODATA`. @_alwaysEmitIntoClient - public static var noData: Errno { Errno(_ENODATA) } + public static var noData: Errno { .init(rawValue: _ENODATA) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noData") public static var ENODATA: Errno { noData } +#endif /// Reserved. /// @@ -1306,19 +1314,20 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOLINK`. @_alwaysEmitIntoClient - public static var noLink: Errno { Errno(_ENOLINK) } + public static var noLink: Errno { .init(rawValue: _ENOLINK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noLink") public static var ENOLINK: Errno { noLink } +#if !os(WASI) && !os(FreeBSD) /// Reserved. /// /// This error is reserved for future use. /// /// The corresponding C error is `ENOSR`. @_alwaysEmitIntoClient - public static var noStreamResources: Errno { Errno(_ENOSR) } + public static var noStreamResources: Errno { .init(rawValue: _ENOSR) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noStreamResources") @@ -1330,11 +1339,13 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `ENOSTR`. @_alwaysEmitIntoClient - public static var notStream: Errno { Errno(_ENOSTR) } + public static var notStream: Errno { .init(rawValue: _ENOSTR) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "notStream") public static var ENOSTR: Errno { notStream } +#endif +#endif /// Protocol error. /// @@ -1344,23 +1355,25 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EPROTO`. @_alwaysEmitIntoClient - public static var protocolError: Errno { Errno(_EPROTO) } + public static var protocolError: Errno { .init(rawValue: _EPROTO) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "protocolError") public static var EPROTO: Errno { protocolError } +#if !os(OpenBSD) && !os(WASI) && !os(FreeBSD) /// Reserved. /// /// This error is reserved for future use. /// /// The corresponding C error is `ETIME`. @_alwaysEmitIntoClient - public static var timeout: Errno { Errno(_ETIME) } + public static var timeout: Errno { .init(rawValue: _ETIME) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "timeout") public static var ETIME: Errno { timeout } +#endif #endif /// Operation not supported on socket. @@ -1370,7 +1383,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// /// The corresponding C error is `EOPNOTSUPP`. @_alwaysEmitIntoClient - public static var notSupportedOnSocket: Errno { Errno(_EOPNOTSUPP) } + public static var notSupportedOnSocket: Errno { .init(rawValue: _EOPNOTSUPP) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "notSupportedOnSocket") @@ -1378,24 +1391,24 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { } // Constants defined in header but not man page -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension Errno { - /// Operation would block. /// /// The corresponding C error is `EWOULDBLOCK`. @_alwaysEmitIntoClient - public static var wouldBlock: Errno { Errno(_EWOULDBLOCK) } + public static var wouldBlock: Errno { .init(rawValue: _EWOULDBLOCK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "wouldBlock") public static var EWOULDBLOCK: Errno { wouldBlock } +#if !os(WASI) /// Too many references: can't splice. /// /// The corresponding C error is `ETOOMANYREFS`. @_alwaysEmitIntoClient - public static var tooManyReferences: Errno { Errno(_ETOOMANYREFS) } + public static var tooManyReferences: Errno { .init(rawValue: _ETOOMANYREFS) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "tooManyReferences") @@ -1405,18 +1418,19 @@ extension Errno { /// /// The corresponding C error is `EREMOTE`. @_alwaysEmitIntoClient - public static var tooManyRemoteLevels: Errno { Errno(_EREMOTE) } + public static var tooManyRemoteLevels: Errno { .init(rawValue: _EREMOTE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "tooManyRemoteLevels") public static var EREMOTE: Errno { tooManyRemoteLevels } +#endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN /// No such policy registered. /// /// The corresponding C error is `ENOPOLICY`. @_alwaysEmitIntoClient - public static var noSuchPolicy: Errno { Errno(_ENOPOLICY) } + public static var noSuchPolicy: Errno { .init(rawValue: _ENOPOLICY) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noSuchPolicy") @@ -1428,7 +1442,7 @@ extension Errno { /// /// The corresponding C error is `ENOTRECOVERABLE`. @_alwaysEmitIntoClient - public static var notRecoverable: Errno { Errno(_ENOTRECOVERABLE) } + public static var notRecoverable: Errno { .init(rawValue: _ENOTRECOVERABLE) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "notRecoverable") @@ -1438,24 +1452,58 @@ extension Errno { /// /// The corresponding C error is `EOWNERDEAD`. @_alwaysEmitIntoClient - public static var previousOwnerDied: Errno { Errno(_EOWNERDEAD) } + public static var previousOwnerDied: Errno { .init(rawValue: _EOWNERDEAD) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "previousOwnerDied") public static var EOWNERDEAD: Errno { previousOwnerDied } #endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if os(FreeBSD) + /// Capabilities insufficient. + /// + /// The corresponding C error is `ENOTCAPABLE`. + @_alwaysEmitIntoClient + public static var notCapable: Errno { .init(rawValue: _ENOTCAPABLE) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "notCapable") + public static var ENOTCAPABLE: Errno { notCapable } + + /// Not permitted in capability mode. + /// + /// The corresponding C error is `ECAPMODE`. + @_alwaysEmitIntoClient + public static var capabilityMode: Errno { .init(rawValue: _ECAPMODE) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "capabilityMode") + public static var ECAPMODE: Errno { capabilityMode } + + /// Integrity check failed. + /// + /// The corresponding C error is `EINTEGRITY`. + @_alwaysEmitIntoClient + public static var integrityCheckFailed: Errno { .init(rawValue: _EINTEGRITY) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "integrityCheckFailed") + public static var EINTEGRITY: Errno { integrityCheckFailed } +#endif + +#if SYSTEM_PACKAGE_DARWIN /// Interface output queue is full. /// /// The corresponding C error is `EQFULL`. @_alwaysEmitIntoClient - public static var outputQueueFull: Errno { Errno(_EQFULL) } + public static var outputQueueFull: Errno { .init(rawValue: _EQFULL) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "outputQueueFull") public static var EQFULL: Errno { outputQueueFull } +#endif +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// The largest valid error. /// /// This value is the largest valid value @@ -1464,7 +1512,7 @@ extension Errno { /// /// The corresponding C error is `ELAST`. @_alwaysEmitIntoClient - public static var lastErrnoValue: Errno { Errno(_ELAST) } + public static var lastErrnoValue: Errno { .init(rawValue: _ELAST) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "lastErrnoValue") @@ -1472,8 +1520,7 @@ extension Errno { #endif } -@_implementationOnly import SystemInternals -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension Errno { // TODO: We want to provide safe access to `errno`, but we need a // release-barrier to do so. @@ -1488,14 +1535,14 @@ extension Errno { } // Use "hidden" entry points for `NSError` bridging -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension Errno { public var _code: Int { Int(rawValue) } public var _domain: String { "NSPOSIXErrorDomain" } } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension Errno: CustomStringConvertible, CustomDebugStringConvertible { /// A textual representation of the most recent error /// returned by a system call. @@ -1515,7 +1562,7 @@ extension Errno: CustomStringConvertible, CustomDebugStringConvertible { public var debugDescription: String { self.description } } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension Errno { @_alwaysEmitIntoClient public static func ~=(_ lhs: Errno, _ rhs: Error) -> Bool { diff --git a/Sources/System/ErrnoWindows.swift b/Sources/System/ErrnoWindows.swift new file mode 100644 index 00000000..cc481cb7 --- /dev/null +++ b/Sources/System/ErrnoWindows.swift @@ -0,0 +1,20 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 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 WinSDK + +extension Errno { + internal init(windowsError: DWORD) { + self.init(rawValue: _mapWindowsErrorToErrno(windowsError)) + } +} + +#endif diff --git a/Sources/System/FileDescriptor.swift b/Sources/System/FileDescriptor.swift index 031a6abd..d5f5883b 100644 --- a/Sources/System/FileDescriptor.swift +++ b/Sources/System/FileDescriptor.swift @@ -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 - 2021 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 @@ -14,7 +14,7 @@ /// of `FileDescriptor` values, /// in the same way as you manage a raw C file handle. @frozen -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) public struct FileDescriptor: RawRepresentable, Hashable, Codable { /// The raw C file handle. @_alwaysEmitIntoClient @@ -25,11 +25,28 @@ public struct FileDescriptor: RawRepresentable, Hashable, Codable { public init(rawValue: CInt) { self.rawValue = rawValue } } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +// Standard file descriptors. +@available(System 0.0.1, *) +extension FileDescriptor { + /// The standard input file descriptor, with a numeric value of 0. + @_alwaysEmitIntoClient + public static var standardInput: FileDescriptor { .init(rawValue: 0) } + + /// The standard output file descriptor, with a numeric value of 1. + @_alwaysEmitIntoClient + public static var standardOutput: FileDescriptor { .init(rawValue: 1) } + + /// The standard error file descriptor, with a numeric value of 2. + @_alwaysEmitIntoClient + public static var standardError: FileDescriptor { .init(rawValue: 2) } +} + +@available(System 0.0.1, *) extension FileDescriptor { /// The desired read and write access for a newly opened file. @frozen - public struct AccessMode: RawRepresentable, Hashable, Codable { + @available(System 0.0.1, *) + public struct AccessMode: RawRepresentable, Sendable, Hashable, Codable { /// The raw C access mode. @_alwaysEmitIntoClient public var rawValue: CInt @@ -71,7 +88,8 @@ extension FileDescriptor { /// Options that specify behavior for a newly-opened file. @frozen - public struct OpenOptions: OptionSet, Hashable, Codable { + @available(System 0.0.1, *) + public struct OpenOptions: OptionSet, Sendable, Hashable, Codable { /// The raw C options. @_alwaysEmitIntoClient public var rawValue: CInt @@ -80,9 +98,6 @@ extension FileDescriptor { @_alwaysEmitIntoClient public init(rawValue: CInt) { self.rawValue = rawValue } - @_alwaysEmitIntoClient - private init(_ raw: CInt) { self.init(rawValue: raw) } - #if !os(Windows) /// Indicates that opening the file doesn't /// wait for the file or device to become available. @@ -91,7 +106,7 @@ extension FileDescriptor { /// the system doesn't wait for the device or file /// to be ready or available. /// If the - /// + /// /// call would result in the process being blocked for some reason, /// that method returns immediately. /// This flag also has the effect of making all @@ -99,7 +114,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `O_NONBLOCK`. @_alwaysEmitIntoClient - public static var nonBlocking: OpenOptions { OpenOptions(_O_NONBLOCK) } + public static var nonBlocking: OpenOptions { .init(rawValue: _O_NONBLOCK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "nonBlocking") @@ -115,7 +130,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `O_APPEND`. @_alwaysEmitIntoClient - public static var append: OpenOptions { OpenOptions(_O_APPEND) } + public static var append: OpenOptions { .init(rawValue: _O_APPEND) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "append") @@ -125,7 +140,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `O_CREAT`. @_alwaysEmitIntoClient - public static var create: OpenOptions { OpenOptions(_O_CREAT) } + public static var create: OpenOptions { .init(rawValue: _O_CREAT) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "create") @@ -139,7 +154,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `O_TRUNC`. @_alwaysEmitIntoClient - public static var truncate: OpenOptions { OpenOptions(_O_TRUNC) } + public static var truncate: OpenOptions { .init(rawValue: _O_TRUNC) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "truncate") @@ -149,25 +164,25 @@ extension FileDescriptor { /// expecting that it doesn't exist. /// /// If this option and ``create`` are both specified and the file exists, - /// + /// /// returns an error instead of creating the file. /// You can use this, for example, /// to implement a simple exclusive-access locking mechanism. /// /// If this option and ``create`` are both specified /// and the last component of the file's path is a symbolic link, - /// + /// /// fails even if the symbolic link points to a nonexistent name. /// /// The corresponding C constant is `O_EXCL`. @_alwaysEmitIntoClient - public static var exclusiveCreate: OpenOptions { OpenOptions(_O_EXCL) } + public static var exclusiveCreate: OpenOptions { .init(rawValue: _O_EXCL) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "exclusiveCreate") public static var O_EXCL: OpenOptions { exclusiveCreate } -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Indicates that opening the file /// atomically obtains a shared lock on the file. /// @@ -179,7 +194,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `O_SHLOCK`. @_alwaysEmitIntoClient - public static var sharedLock: OpenOptions { OpenOptions(_O_SHLOCK) } + public static var sharedLock: OpenOptions { .init(rawValue: _O_SHLOCK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "sharedLock") @@ -196,7 +211,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `O_EXLOCK`. @_alwaysEmitIntoClient - public static var exclusiveLock: OpenOptions { OpenOptions(_O_EXLOCK) } + public static var exclusiveLock: OpenOptions { .init(rawValue: _O_EXLOCK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "exclusiveLock") @@ -208,32 +223,62 @@ extension FileDescriptor { /// /// If you specify this option /// and the file path you pass to - /// + /// /// is a symbolic link, /// then that open operation fails. /// /// The corresponding C constant is `O_NOFOLLOW`. @_alwaysEmitIntoClient - public static var noFollow: OpenOptions { OpenOptions(_O_NOFOLLOW) } + public static var noFollow: OpenOptions { .init(rawValue: _O_NOFOLLOW) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "noFollow") public static var O_NOFOLLOW: OpenOptions { noFollow } + + /// Indicates that opening the file only succeeds if the file is a directory. + /// + /// If you specify this option and the file path you pass to + /// + /// is a not a directory, then that open operation fails. + /// + /// The corresponding C constant is `O_DIRECTORY`. + @_alwaysEmitIntoClient + public static var directory: OpenOptions { .init(rawValue: _O_DIRECTORY) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "directory") + public static var O_DIRECTORY: OpenOptions { directory } #endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if os(FreeBSD) + /// Indicates that each write operation is synchronous. + /// + /// If this option is specified, + /// each time you write to the file, + /// the new data is written immediately and synchronously to the disk. + /// + /// The corresponding C constant is `O_SYNC`. + @_alwaysEmitIntoClient + public static var sync: OpenOptions { .init(rawValue: _O_SYNC) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "sync") + public static var O_SYNC: OpenOptions { sync } +#endif + +#if SYSTEM_PACKAGE_DARWIN /// Indicates that opening the file /// opens symbolic links instead of following them. /// /// If you specify this option /// and the file path you pass to - /// + /// /// is a symbolic link, /// then the link itself is opened instead of what it links to. /// /// The corresponding C constant is `O_SYMLINK`. @_alwaysEmitIntoClient - public static var symlink: OpenOptions { OpenOptions(_O_SYMLINK) } + public static var symlink: OpenOptions { .init(rawValue: _O_SYMLINK) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "symlink") @@ -249,7 +294,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `O_EVTONLY`. @_alwaysEmitIntoClient - public static var eventOnly: OpenOptions { OpenOptions(_O_EVTONLY) } + public static var eventOnly: OpenOptions { .init(rawValue: _O_EVTONLY) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "eventOnly") @@ -271,7 +316,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `O_CLOEXEC`. @_alwaysEmitIntoClient - public static var closeOnExec: OpenOptions { OpenOptions(_O_CLOEXEC) } + public static var closeOnExec: OpenOptions { .init(rawValue: _O_CLOEXEC) } @_alwaysEmitIntoClient @available(*, unavailable, renamed: "closeOnExec") @@ -281,7 +326,8 @@ extension FileDescriptor { /// Options for specifying what a file descriptor's offset is relative to. @frozen - public struct SeekOrigin: RawRepresentable, Hashable, Codable { + @available(System 0.0.1, *) + public struct SeekOrigin: RawRepresentable, Sendable, Hashable, Codable { /// The raw C value. @_alwaysEmitIntoClient public var rawValue: CInt @@ -324,7 +370,7 @@ extension FileDescriptor { // TODO: These are available on some versions of Linux with appropriate // macro defines. -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Indicates that the offset should be set /// to the next hole after the specified number of bytes. /// @@ -356,7 +402,7 @@ extension FileDescriptor { } } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension FileDescriptor.AccessMode : CustomStringConvertible, CustomDebugStringConvertible { @@ -375,7 +421,7 @@ extension FileDescriptor.AccessMode public var debugDescription: String { self.description } } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension FileDescriptor.SeekOrigin : CustomStringConvertible, CustomDebugStringConvertible { @@ -386,7 +432,7 @@ extension FileDescriptor.SeekOrigin case .start: return "start" case .current: return "current" case .end: return "end" -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN case .nextHole: return "nextHole" case .nextData: return "nextData" #endif @@ -398,14 +444,14 @@ extension FileDescriptor.SeekOrigin public var debugDescription: String { self.description } } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension FileDescriptor.OpenOptions : CustomStringConvertible, CustomDebugStringConvertible { /// A textual representation of the open options. @inline(never) public var description: String { -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN let descriptions: [(Element, StaticString)] = [ (.nonBlocking, ".nonBlocking"), (.append, ".append"), @@ -426,6 +472,19 @@ extension FileDescriptor.OpenOptions (.truncate, ".truncate"), (.exclusiveCreate, ".exclusiveCreate"), ] +#elseif os(FreeBSD) + let descriptions: [(Element, StaticString)] = [ + (.nonBlocking, ".nonBlocking"), + (.append, ".append"), + (.create, ".create"), + (.truncate, ".truncate"), + (.exclusiveCreate, ".exclusiveCreate"), + (.sharedLock, ".sharedLock"), + (.exclusiveLock, ".exclusiveLock"), + (.sync, ".sync"), + (.noFollow, ".noFollow"), + (.closeOnExec, ".closeOnExec") + ] #else let descriptions: [(Element, StaticString)] = [ (.nonBlocking, ".nonBlocking"), @@ -444,3 +503,8 @@ extension FileDescriptor.OpenOptions /// A textual representation of the open options, suitable for debugging. public var debugDescription: String { self.description } } + +// The decision on whether to make FileDescriptor Sendable or not +// is currently being discussed in https://github.com/apple/swift-system/pull/112 +//@available(*, unavailable, message: "File descriptors are not completely thread-safe.") +//extension FileDescriptor: Sendable {} diff --git a/Sources/System/FileHelpers.swift b/Sources/System/FileHelpers.swift index d083c101..5b082766 100644 --- a/Sources/System/FileHelpers.swift +++ b/Sources/System/FileHelpers.swift @@ -7,7 +7,7 @@ See https://swift.org/LICENSE.txt for license information */ -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension FileDescriptor { /// Runs a closure and then closes the file descriptor, even if an error occurs. /// @@ -46,9 +46,6 @@ extension FileDescriptor { /// increments that position by the number of bytes written. /// See also ``seek(offset:from:)``. /// - /// This method either writes the entire contents of `sequence`, - /// or throws an error if only part of the content was written. - /// /// If `sequence` doesn't implement /// the method, /// temporary space will be allocated as needed. diff --git a/Sources/System/FileOperations.swift b/Sources/System/FileOperations.swift index b1ce2358..9ddc16c3 100644 --- a/Sources/System/FileOperations.swift +++ b/Sources/System/FileOperations.swift @@ -1,15 +1,13 @@ /* 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 - 2025 Apple Inc. and the Swift System project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information */ -@_implementationOnly import SystemInternals - -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension FileDescriptor { /// Opens or creates a file for reading or writing. /// @@ -33,12 +31,23 @@ extension FileDescriptor { permissions: FilePermissions? = nil, retryOnInterrupt: Bool = true ) throws -> FileDescriptor { - try path.withCString { + #if !os(Windows) + return try path.withCString { + try FileDescriptor.open( + $0, mode, options: options, permissions: permissions, retryOnInterrupt: retryOnInterrupt) + } + #else + return try path.withPlatformString { try FileDescriptor.open( $0, mode, options: options, permissions: permissions, retryOnInterrupt: retryOnInterrupt) } + #endif } + #if !os(Windows) + // On Darwin, `CInterop.PlatformChar` is less available than + // `FileDescriptor.open`, so we need to use `CChar` instead. + /// Opens or creates a file for reading or writing. /// /// - Parameters: @@ -72,7 +81,7 @@ extension FileDescriptor { _ mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions, permissions: FilePermissions?, - retryOnInterrupt: Bool = true + retryOnInterrupt: Bool ) -> Result { let oFlag = mode.rawValue | options.rawValue let descOrError: Result = valueOrErrno(retryOnInterrupt: retryOnInterrupt) { @@ -85,6 +94,52 @@ extension FileDescriptor { } return descOrError.map { FileDescriptor(rawValue: $0) } } + #else + /// Opens or creates a file for reading or writing. + /// + /// - Parameters: + /// - path: The location of the file to open. + /// - mode: The read and write access to use. + /// - options: The behavior for opening the file. + /// - permissions: The file permissions to use for created files. + /// - retryOnInterrupt: Whether to retry the open operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: A file descriptor for the open file + /// + /// The corresponding C function is `open`. + @_alwaysEmitIntoClient + public static func open( + _ path: UnsafePointer, + _ mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + retryOnInterrupt: Bool = true + ) throws -> FileDescriptor { + try FileDescriptor._open( + path, mode, options: options, permissions: permissions, retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _open( + _ path: UnsafePointer, + _ mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions, + permissions: FilePermissions?, + retryOnInterrupt: Bool + ) -> Result { + let oFlag = mode.rawValue | options.rawValue + let descOrError: Result = valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + if let permissions = permissions { + return system_open(path, oFlag, permissions.rawValue) + } + return system_open(path, oFlag) + } + return descOrError.map { FileDescriptor(rawValue: $0) } + } + #endif /// Deletes a file descriptor. /// @@ -98,10 +153,10 @@ extension FileDescriptor { @usableFromInline internal func _close() -> Result<(), Errno> { - nothingOrErrno(system_close(self.rawValue)) + nothingOrErrno(retryOnInterrupt: false) { system_close(self.rawValue) } } - /// Reposition the offset for the given file descriptor. + /// Repositions the offset for the given file descriptor. /// /// - Parameters: /// - offset: The new offset for the file descriptor. @@ -122,8 +177,9 @@ extension FileDescriptor { internal func _seek( offset: Int64, from whence: FileDescriptor.SeekOrigin ) -> Result { - let newOffset = system_lseek(self.rawValue, COffT(offset), whence.rawValue) - return valueOrErrno(Int64(newOffset)) + valueOrErrno(retryOnInterrupt: false) { + Int64(system_lseek(self.rawValue, _COffT(offset), whence.rawValue)) + } } @@ -145,7 +201,7 @@ extension FileDescriptor { /// Pass `false` to try only once and throw an error upon interruption. /// - Returns: The number of bytes that were read. /// - /// The property of `buffer` + /// The property of `buffer` /// determines the maximum number of bytes that are read into that buffer. /// /// After reading, @@ -165,7 +221,7 @@ extension FileDescriptor { @usableFromInline internal func _read( into buffer: UnsafeMutableRawBufferPointer, - retryOnInterrupt: Bool = true + retryOnInterrupt: Bool ) throws -> Result { valueOrErrno(retryOnInterrupt: retryOnInterrupt) { system_read(self.rawValue, buffer.baseAddress, buffer.count) @@ -183,10 +239,10 @@ extension FileDescriptor { /// Pass `false` to try only once and throw an error upon interruption. /// - Returns: The number of bytes that were read. /// - /// The property of `buffer` + /// The property of `buffer` /// determines the maximum number of bytes that are read into that buffer. /// - /// Unlike , + /// Unlike , /// this method leaves the file's existing offset unchanged. /// /// The corresponding C function is `pread`. @@ -210,7 +266,7 @@ extension FileDescriptor { retryOnInterrupt: Bool ) -> Result { valueOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_pread(self.rawValue, buffer.baseAddress, buffer.count, COffT(offset)) + system_pread(self.rawValue, buffer.baseAddress, buffer.count, _COffT(offset)) } } @@ -292,7 +348,7 @@ extension FileDescriptor { retryOnInterrupt: Bool ) -> Result { valueOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_pwrite(self.rawValue, buffer.baseAddress, buffer.count, COffT(offset)) + system_pwrite(self.rawValue, buffer.baseAddress, buffer.count, _COffT(offset)) } } @@ -310,3 +366,188 @@ extension FileDescriptor { retryOnInterrupt: retryOnInterrupt) } } + +#if !os(WASI) +@available(System 0.0.2, *) +extension FileDescriptor { + /// Duplicates this file descriptor and return the newly created copy. + /// + /// - Parameters: + /// - `target`: The desired target file descriptor, or `nil`, in which case + /// the copy is assigned to the file descriptor with the lowest raw value + /// that is not currently in use by the process. + /// - retryOnInterrupt: Whether to retry the write operation + /// if it throws ``Errno/interrupted``. The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The new file descriptor. + /// + /// If the `target` descriptor is already in use, then it is first + /// deallocated as if a close(2) call had been done first. + /// + /// File descriptors are merely references to some underlying system resource. + /// The system does not distinguish between the original and the new file + /// descriptor in any way. For example, read, write and seek operations on + /// one of them also affect the logical file position in the other, and + /// append mode, non-blocking I/O and asynchronous I/O options are shared + /// between the references. If a separate pointer into the file is desired, + /// a different object reference to the file must be obtained by issuing an + /// additional call to `open`. + /// + /// However, each file descriptor maintains its own close-on-exec flag. + /// + /// + /// The corresponding C functions are `dup` and `dup2`. + @_alwaysEmitIntoClient + @available(System 0.0.2, *) + public func duplicate( + as target: FileDescriptor? = nil, + retryOnInterrupt: Bool = true + ) throws -> FileDescriptor { + try _duplicate(as: target, retryOnInterrupt: retryOnInterrupt).get() + } + + @available(System 0.0.2, *) + @usableFromInline + internal func _duplicate( + as target: FileDescriptor?, + retryOnInterrupt: Bool + ) throws -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + if let target = target { + return system_dup2(self.rawValue, target.rawValue) + } + return system_dup(self.rawValue) + }.map(FileDescriptor.init(rawValue:)) + } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "duplicate") + public func dup() throws -> FileDescriptor { + fatalError("Not implemented") + } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "duplicate") + public func dup2() throws -> FileDescriptor { + fatalError("Not implemented") + } +} +#endif + +#if !os(WASI) +@available(System 1.1.0, *) +extension FileDescriptor { + /// Creates a unidirectional data channel, which can be used for interprocess communication. + /// + /// - Returns: The pair of file descriptors. + /// + /// The corresponding C function is `pipe`. + @_alwaysEmitIntoClient + @available(System 1.1.0, *) + public static func pipe() throws -> (readEnd: FileDescriptor, writeEnd: FileDescriptor) { + try _pipe().get() + } + + @available(System 1.1.0, *) + @usableFromInline + internal static func _pipe() -> Result<(readEnd: FileDescriptor, writeEnd: FileDescriptor), Errno> { + var fds: (Int32, Int32) = (-1, -1) + return withUnsafeMutablePointer(to: &fds) { pointer in + pointer.withMemoryRebound(to: Int32.self, capacity: 2) { fds in + valueOrErrno(retryOnInterrupt: false) { + system_pipe(fds) + }.map { _ in (.init(rawValue: fds[0]), .init(rawValue: fds[1])) } + } + } + } +} +#endif + +@available(System 1.2.0, *) +extension FileDescriptor { + /// Truncates or extends the file referenced by this file descriptor. + /// + /// - Parameters: + /// - newSize: The length in bytes to resize the file to. + /// - retryOnInterrupt: Whether to retry the write operation + /// if it throws ``Errno/interrupted``. The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// + /// The file referenced by this file descriptor will by truncated (or extended) to `newSize`. + /// + /// If the current size of the file exceeds `newSize`, any extra data is discarded. If the current + /// size of the file is smaller than `newSize`, the file is extended and filled with zeros to the + /// provided size. + /// + /// This function requires that the file has been opened for writing. + /// + /// - Note: This function does not modify the current offset for any open file descriptors + /// associated with the file. + /// + /// The corresponding C function is `ftruncate`. + @available(System 1.2.0, *) + @_alwaysEmitIntoClient + public func resize( + to newSize: Int64, + retryOnInterrupt: Bool = true + ) throws { + try _resize( + to: newSize, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @available(System 1.2.0, *) + @usableFromInline + internal func _resize( + to newSize: Int64, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_ftruncate(self.rawValue, _COffT(newSize)) + } + } +} + +#if !os(WASI) // WASI has no umask +extension FilePermissions { + /// The file creation permission mask (aka "umask"). + /// + /// Permissions set in this mask will be cleared by functions that create + /// files or directories. Note that this mask is process-wide, and that + /// *getting* it is not thread safe. + internal static var creationMask: FilePermissions { + get { + let oldMask = _umask(0o22) + _ = _umask(oldMask) + return FilePermissions(rawValue: oldMask) + } + set { + _ = _umask(newValue.rawValue) + } + } + + /// Change the file creation permission mask, run some code, then + /// restore it to its original value. + /// + /// - Parameters: + /// - permissions: The new permission mask. + /// + /// This is more efficient than reading `creationMask` and restoring it + /// afterwards, because of the way reading the creation mask works. + internal static func withCreationMask( + _ permissions: FilePermissions, + body: () throws -> R + ) rethrows -> R { + let oldMask = _umask(permissions.rawValue) + defer { + _ = _umask(oldMask) + } + return try body() + } + + internal static func _umask(_ mode: CModeT) -> CModeT { + return system_umask(mode) + } +} +#endif diff --git a/Sources/System/FilePath.swift b/Sources/System/FilePath.swift deleted file mode 100644 index 2fa7cc83..00000000 --- a/Sources/System/FilePath.swift +++ /dev/null @@ -1,210 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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 -*/ - -extension UnsafePointer where Pointee == UInt8 { - internal var _asCChar: UnsafePointer { - UnsafeRawPointer(self).assumingMemoryBound(to: CChar.self) - } -} -extension UnsafePointer where Pointee == CChar { - internal var _asUInt8: UnsafePointer { - UnsafeRawPointer(self).assumingMemoryBound(to: UInt8.self) - } -} -extension UnsafeBufferPointer where Element == UInt8 { - internal var _asCChar: UnsafeBufferPointer { - let base = baseAddress?._asCChar - return UnsafeBufferPointer(start: base, count: self.count) - } -} -extension UnsafeBufferPointer where Element == CChar { - internal var _asUInt8: UnsafeBufferPointer { - let base = baseAddress?._asUInt8 - return UnsafeBufferPointer(start: base, count: self.count) - } -} - -// NOTE: FilePath not frozen for ABI flexibility - -/// A null-terminated sequence of bytes -/// that represents a location in the file system. -/// -/// This structure doesn't give any meaning to the bytes that it contains, -/// except for the requirement that the last byte is a NUL (`0x0`). -/// The file system defines how this string is interpreted; -/// for example, by its choice of string encoding. -/// -/// The code below creates a file path from a string literal, -/// and then uses it to open and append to a log file: -/// -/// let message: String = "This is a log message." -/// let path: FilePath = "/tmp/log" -/// let fd = try FileDescriptor.open(path, .writeOnly, options: .append) -/// try fd.closeAfter { try fd.writeAll(message.utf8) } -/// -/// File paths conform to the -/// and -/// and protocols -/// by performing the protocols' operations on their raw byte contents. -/// This conformance allows file paths to be used, -/// for example, as keys in a dictionary. -/// However, the rules for path equivalence -/// are file-system–specific and have additional considerations -/// like case insensitivity, Unicode normalization, and symbolic links. -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -public struct FilePath { - internal var bytes: [CChar] - - /// Creates an empty, null-terminated path. - public init() { - self.bytes = [0] - _invariantCheck() - } -} - -// -// MARK: - Public Interfaces -// -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -extension FilePath { - /// The length of the file path, excluding the null termination. - public var length: Int { bytes.count - 1 } -} - -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -extension FilePath { - internal init(nullTerminatedBytes: C) where C.Element == CChar { - self.bytes = Array(nullTerminatedBytes) - _invariantCheck() - } - - internal init(byteContents bytes: C) where C.Element == CChar { - var nulTermBytes = Array(bytes) - nulTermBytes.append(0) - self.init(nullTerminatedBytes: nulTermBytes) - } -} - -@_implementationOnly import SystemInternals - -// -// MARK: - CString interfaces -// -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -extension FilePath { - /// Creates a file path by copying bytes from a null-terminated C string. - /// - /// - Parameter cString: A pointer to a null-terminated C string. - public init(cString: UnsafePointer) { - self.init(nullTerminatedBytes: - UnsafeBufferPointer(start: cString, count: 1 + system_strlen(cString))) - } - - /// Calls the given closure with a pointer to the contents of the file path, - /// represented as a null-terminated C string. - /// - /// - Parameter body: A closure with a pointer parameter - /// that points to a null-terminated C string. - /// If `body` has a return value, - /// that value is also used as the return value for this method. - /// - Returns: The return value, if any, of the `body` closure parameter. - /// - /// The pointer passed as an argument to `body` is valid - /// only during the execution of this method. - /// Don't try to store the pointer for later use. - public func withCString( - _ body: (UnsafePointer) throws -> Result - ) rethrows -> Result { - try bytes.withUnsafeBufferPointer { try body($0.baseAddress!) } - } - - // TODO: in the future, with opaque result types with associated - // type constraints, we want to provide a RAC for terminated - // byte contents and unterminated byte contents. -} - -// -// MARK: - String interfaces -// -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -extension FilePath: ExpressibleByStringLiteral { - /// Creates a file path from a string literal. - /// - /// - Parameter stringLiteral: A string literal - /// whose UTF-8 contents to use as the contents of the path. - public init(stringLiteral: String) { - self.init(stringLiteral) - } - - /// Creates a file path from a string. - /// - /// - Parameter string: A string - /// whose UTF-8 contents to use as the contents of the path. - public init(_ string: String) { - var str = string - self = str.withUTF8 { FilePath(byteContents: $0._asCChar) } - } -} - -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -extension String { - /// Creates a string by interpreting the file path's content as UTF-8. - /// - /// - Parameter path: The file path to be interpreted as UTF-8. - /// - /// If the content of the file path - /// isn't a well-formed UTF-8 string, - /// this initializer removes invalid bytes or replaces them with U+FFFD. - /// This means that, depending on the semantics of the specific file system, - /// conversion to a string and back to a path - /// might result in a value that's different from the original path. - public init(decoding path: FilePath) { - self = path.withCString { String(cString: $0) } - } - - @available(*, deprecated, renamed: "String.init(decoding:)") - public init(_ path: FilePath) { - self.init(decoding: path) - } - - /// Creates a string from a file path, validating its UTF-8 contents. - /// - /// - Parameter path: The file path be interpreted as UTF-8. - /// - /// If the contents of the file path - /// isn't a well-formed UTF-8 string, - /// this initializer returns `nil`. - public init?(validatingUTF8 path: FilePath) { - guard let str = path.withCString({ String(validatingUTF8: $0) }) else { - return nil - } - self = str - } -} - -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -extension FilePath: CustomStringConvertible, CustomDebugStringConvertible { - /// A textual representation of the file path. - @inline(never) - public var description: String { String(decoding: self) } - - /// A textual representation of the file path, suitable for debugging. - public var debugDescription: String { self.description.debugDescription } -} - -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -extension FilePath: Hashable, Codable {} - -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -extension FilePath { - fileprivate func _invariantCheck() { - precondition(bytes.last! == 0) - _debugPrecondition(bytes.firstIndex(of: 0) == bytes.count - 1) - } -} diff --git a/Sources/System/FilePath/FilePath.swift b/Sources/System/FilePath/FilePath.swift new file mode 100644 index 00000000..b27d053a --- /dev/null +++ b/Sources/System/FilePath/FilePath.swift @@ -0,0 +1,90 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +/// Represents a location in the file system. +/// +/// This structure recognizes directory separators (e.g. `/`), roots, and +/// requires that the content terminates in a NUL (`0x0`). Beyond that, it +/// does not give any meaning to the bytes that it contains. The file system +/// defines how the content is interpreted; for example, by its choice of string +/// encoding. +/// +/// On construction, `FilePath` will normalize separators by removing +/// redundant intermediary separators and stripping any trailing separators. +/// On Windows, `FilePath` will also normalize forward slashes `/` into +/// backslashes `\`, as preferred by the platform. +/// +/// The code below creates a file path from a string literal, +/// and then uses it to open and append to a log file: +/// +/// let message: String = "This is a log message." +/// let path: FilePath = "/tmp/log" +/// let fd = try FileDescriptor.open(path, .writeOnly, options: .append) +/// try fd.closeAfter { try fd.writeAll(message.utf8) } +/// +/// File paths conform to the +/// +/// and protocols +/// by performing the protocols' operations on their raw byte contents. +/// This conformance allows file paths to be used, +/// for example, as keys in a dictionary. +/// However, the rules for path equivalence +/// are file-system–specific and have additional considerations +/// like case insensitivity, Unicode normalization, and symbolic links. +@available(System 0.0.1, *) +public struct FilePath: Sendable { + // TODO(docs): Section on all the new syntactic operations, lexical normalization, decomposition, + // components, etc. + internal var _storage: SystemString + + /// Creates an empty, null-terminated path. + public init() { + self._storage = SystemString() + _invariantCheck() + } + + // In addition to the empty init, this init will properly normalize + // separators. All other initializers should be implemented by + // ultimately deferring to a normalizing init. + internal init(_ str: SystemString) { + self._storage = str + self._normalizeSeparators() + _invariantCheck() + } +} + +@available(System 0.0.1, *) +extension FilePath { + /// The length of the file path, excluding the null terminator. + public var length: Int { _storage.length } +} + +@available(System 0.0.1, *) +extension FilePath: Hashable {} + +@available(System 0.0.1, *) +extension FilePath: Codable { + // Encoder is synthesized; it probably should have been explicit and used + // a single-value container, but making that change now is somewhat risky. + + // Decoder is written explicitly to ensure that we validate invariants on + // untrusted input. + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self._storage = try container.decode(SystemString.self, forKey: ._storage) + guard _invariantsSatisfied() else { + throw DecodingError.dataCorruptedError( + forKey: ._storage, + in: container, + debugDescription: + "Encoding does not satisfy the invariants of FilePath" + ) + } + } +} diff --git a/Sources/System/FilePath/FilePathComponentView.swift b/Sources/System/FilePath/FilePathComponentView.swift new file mode 100644 index 00000000..be176305 --- /dev/null +++ b/Sources/System/FilePath/FilePathComponentView.swift @@ -0,0 +1,219 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +// MARK: - API + +@available(System 0.0.2, *) +extension FilePath { + /// A bidirectional, range replaceable collection of the non-root components + /// that make up a file path. + /// + /// ComponentView provides access to standard `BidirectionalCollection` + /// algorithms for accessing components from the front or back, as well as + /// standard `RangeReplaceableCollection` algorithms for modifying the + /// file path using component or range of components granularity. + /// + /// Example: + /// + /// var path: FilePath = "/./home/./username/scripts/./tree" + /// let scriptIdx = path.components.lastIndex(of: "scripts")! + /// path.components.insert("bin", at: scriptIdx) + /// // path is "/./home/./username/bin/scripts/./tree" + /// + /// path.components.removeAll { $0.kind == .currentDirectory } + /// // path is "/home/username/bin/scripts/tree" + @available(System 0.0.2, *) + public struct ComponentView: Sendable { + internal var _path: FilePath + internal var _start: SystemString.Index + + internal init(_ path: FilePath) { + self._path = path + self._start = path._relativeStart + _invariantCheck() + } + } + + /// View the non-root components that make up this path. + public var components: ComponentView { + __consuming get { ComponentView(self) } + _modify { + // RRC's empty init means that we can't guarantee that the yielded + // view will restore our root. So copy it out first. + // + // TODO(perf): Small-form root (especially on Unix). Have Root + // always copy out (not worth ref counting). Make sure that we're + // not needlessly sliding values around or triggering a COW + let rootStr = self.root?._systemString ?? SystemString() + var comp = ComponentView(self) + self = FilePath() + defer { + self = comp._path + if root?._slice.elementsEqual(rootStr) != true { + self.root = Root(rootStr) + } + } + yield &comp + } + } +} + +@available(System 0.0.2, *) +extension FilePath.ComponentView: BidirectionalCollection { + public typealias Element = FilePath.Component + + @available(System 0.0.2, *) + public struct Index: Sendable, Comparable, Hashable { + internal typealias Storage = SystemString.Index + + internal var _storage: Storage + + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs._storage < rhs._storage + } + + fileprivate init(_ idx: Storage) { + self._storage = idx + } + } + + public var startIndex: Index { Index(_start) } + public var endIndex: Index { Index(_path._storage.endIndex) } + + public func index(after i: Index) -> Index { + return Index(_path._parseComponent(startingAt: i._storage).nextStart) + } + + public func index(before i: Index) -> Index { + Index(_path._parseComponent(priorTo: i._storage).lowerBound) + } + + public subscript(position: Index) -> FilePath.Component { + let end = _path._parseComponent(startingAt: position._storage).componentEnd + return FilePath.Component(_path, position._storage ..< end) + } +} + +@available(System 0.0.2, *) +extension FilePath.ComponentView: RangeReplaceableCollection { + public init() { + self.init(FilePath()) + } + + // TODO(perf): We probably want to have concrete overrides or generic + // specializations taking FP.ComponentView and + // FP.ComponentView.SubSequence because we + // can just memcpy in those cases. We + // probably want to do that for all RRC operations. + + public mutating func replaceSubrange( + _ subrange: Range, with newElements: C + ) where C : Collection, Self.Element == C.Element { + defer { + _path._invariantCheck() + _invariantCheck() + } + if isEmpty { + _path = FilePath(root: _path.root, newElements) + return + } + let range = subrange.lowerBound._storage ..< subrange.upperBound._storage + if newElements.isEmpty { + let fromEnd = subrange.upperBound == endIndex + _path._storage.removeSubrange(range) + if fromEnd { + _path._removeTrailingSeparator() + } + return + } + + // TODO(perf): Avoid extra allocation by sliding elements down and + // filling in the bytes ourselves. + + // If we're inserting at the end, we need a leading separator. + var str = SystemString() + let atEnd = subrange.lowerBound == endIndex + if atEnd { + str.append(platformSeparator) + } + str.appendComponents(components: newElements) + if !atEnd { + str.append(platformSeparator) + } + _path._storage.replaceSubrange(range, with: str) + } +} + +@available(System 0.0.2, *) +extension FilePath { + /// Create a file path from a root and a collection of components. + public init( + root: Root?, _ components: C + ) where C.Element == Component { + var str = root?._systemString ?? SystemString() + str.appendComponents(components: components) + self.init(str) + } + + /// Create a file path from a root and any number of components. + public init(root: Root?, components: Component...) { + self.init(root: root, components) + } + + /// Create a file path from an optional root and a slice of another path's + /// components. + public init(root: Root?, _ components: ComponentView.SubSequence) { + var str = root?._systemString ?? SystemString() + let (start, end) = + (components.startIndex._storage, components.endIndex._storage) + str.append(contentsOf: components.base._slice[start.. { + _start ..< _path._storage.endIndex + } + + internal init(_ str: SystemString) { + fatalError("TODO: consider dropping proto req") + } +} + +// MARK: - Invariants + +@available(System 0.0.2, *) +extension FilePath.ComponentView { + internal func _invariantCheck() { + #if DEBUG + if isEmpty { + precondition(_path.isEmpty == (_path.root == nil)) + return + } + + // If path has a root, + if _path.root != nil { + precondition(first!._slice.startIndex > _path._storage.startIndex) + precondition(first!._slice.startIndex == _path._relativeStart) + } + + self.forEach { $0._invariantCheck() } + + if let base = last { + precondition(base._slice.endIndex == _path._storage.endIndex) + } + + precondition(FilePath(root: _path.root, self) == _path) + #endif // DEBUG + } +} diff --git a/Sources/System/FilePath/FilePathComponents.swift b/Sources/System/FilePath/FilePathComponents.swift new file mode 100644 index 00000000..f2352617 --- /dev/null +++ b/Sources/System/FilePath/FilePathComponents.swift @@ -0,0 +1,290 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2024 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 +*/ + +// MARK: - API + +@available(System 0.0.2, *) +extension FilePath { + /// Represents a root of a file path. + /// + /// On Unix, a root is simply the directory separator `/`. + /// + /// On Windows, a root contains the entire path prefix up to and including + /// the final separator. + /// + /// Examples: + /// * Unix: + /// * `/` + /// * Windows: + /// * `C:\` + /// * `C:` + /// * `\` + /// * `\\server\share\` + /// * `\\?\UNC\server\share\` + /// * `\\?\Volume{12345678-abcd-1111-2222-123445789abc}\` + @available(System 0.0.2, *) + public struct Root: Sendable { + internal var _path: FilePath + internal var _rootEnd: SystemString.Index + + internal init(_ path: FilePath, rootEnd: SystemString.Index) { + self._path = path + self._rootEnd = rootEnd + _invariantCheck() + } + // TODO: Definitely want a small form for this on Windows, + // and intern "/" for Unix. + } + + /// Represents an individual, non-root component of a file path. + /// + /// Components can be one of the special directory components (`.` or `..`) + /// or a file or directory name. Components are never empty and never + /// contain the directory separator. + /// + /// Example: + /// + /// var path: FilePath = "/tmp" + /// let file: FilePath.Component = "foo.txt" + /// file.kind == .regular // true + /// file.extension // "txt" + /// path.append(file) // path is "/tmp/foo.txt" + @available(System 0.0.2, *) + public struct Component: Sendable { + internal var _path: FilePath + internal var _range: Range + + // TODO: Make a small-component form to save on ARC overhead when + // extracted from a path, and especially to save on allocation overhead + // when constructing one from a String literal. + + internal init(_ path: FilePath, _ range: RE) + where RE.Bound == SystemString.Index { + self._path = path + self._range = range.relative(to: path._storage) + precondition(!self._range.isEmpty, "FilePath components cannot be empty") + self._invariantCheck() + } + } +} + +@available(System 0.0.2, *) +extension FilePath.Component { + + /// Whether a component is a regular file or directory name, or a special + /// directory `.` or `..` + @frozen + @available(System 0.0.2, *) + public enum Kind: Sendable { + /// The special directory `.`, representing the current directory. + case currentDirectory + + /// The special directory `..`, representing the parent directory. + case parentDirectory + + /// A file or directory name + case regular + } + + /// The kind of this component + public var kind: Kind { + if _path._isCurrentDirectory(_range) { return .currentDirectory } + if _path._isParentDirectory(_range) { return .parentDirectory } + return .regular + } +} + +@available(System 0.0.2, *) +extension FilePath.Root { + // TODO: Windows analysis APIs +} + +// MARK: - Internals + +extension SystemString { + // TODO: take insertLeadingSlash: Bool + // TODO: turn into an insert operation with slide + internal mutating func appendComponents( + components: C + ) where C.Element == FilePath.Component { + // TODO(perf): Consider pre-pass to count capacity, slide + + defer { + _removeTrailingSeparator() + FilePath(self)._invariantCheck() + } + + for idx in components.indices { + let component = components[idx] + component._withSystemChars { self.append(contentsOf: $0) } + self.append(platformSeparator) + } + } +} + +// Unifying protocol for common functionality between roots, components, +// and views onto SystemString and FilePath. +internal protocol _StrSlice: _PlatformStringable, Hashable, Codable { + var _storage: SystemString { get } + var _range: Range { get } + + init?(_ str: SystemString) + + func _invariantCheck() +} +extension _StrSlice { + internal var _slice: Slice { + Slice(base: _storage, bounds: _range) + } + + internal func _withSystemChars( + _ f: (UnsafeBufferPointer) throws -> T + ) rethrows -> T { + try _storage.withNullTerminatedSystemChars { + try f(UnsafeBufferPointer(rebasing: $0[_range])) + } + } + internal func _withCodeUnits( + _ f: (UnsafeBufferPointer) throws -> T + ) rethrows -> T { + try _slice.withCodeUnits(f) + } + + internal init?(_platformString s: UnsafePointer) { + self.init(SystemString(platformString: s)) + } + + internal func _withPlatformString( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + try _slice.withPlatformString(body) + } + + internal var _systemString: SystemString { SystemString(_slice) } +} +extension _StrSlice { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs._slice.elementsEqual(rhs._slice) + } + public func hash(into hasher: inout Hasher) { + hasher.combine(_slice.count) // discriminator + for element in _slice { + hasher.combine(element) + } + } +} +internal protocol _PathSlice: _StrSlice { + var _path: FilePath { get } +} +extension _PathSlice { + internal var _storage: SystemString { _path._storage } +} + +@available(System 0.0.2, *) +extension FilePath.Component: _PathSlice { +} +@available(System 0.0.2, *) +extension FilePath.Root: _PathSlice { + internal var _range: Range { + (..<_rootEnd).relative(to: _path._storage) + } +} + +@available(System 0.0.1, *) +extension FilePath: _PlatformStringable { + func _withPlatformString(_ body: (UnsafePointer) throws -> Result) rethrows -> Result { + try _storage.withPlatformString(body) + } + + init(_platformString: UnsafePointer) { + self.init(SystemString(platformString: _platformString)) + } + +} + +@available(System 0.0.2, *) +extension FilePath.Component { + // The index of the `.` denoting an extension + internal func _extensionIndex() -> SystemString.Index? { + guard kind == .regular, + let idx = _slice.lastIndex(of: .dot), + idx != _slice.startIndex + else { return nil } + + return idx + } + + internal func _extensionRange() -> Range? { + guard let idx = _extensionIndex() else { return nil } + return _slice.index(after: idx) ..< _slice.endIndex + } + + internal func _stemRange() -> Range { + _slice.startIndex ..< (_extensionIndex() ?? _slice.endIndex) + } +} + +internal func _makeExtension(_ ext: String) -> SystemString { + var result = SystemString() + result.append(.dot) + result.append(contentsOf: ext.unicodeScalars.lazy.map(SystemChar.init)) + return result +} + +@available(System 0.0.2, *) +extension FilePath.Component { + internal init?(_ str: SystemString) { + // FIXME: explicit null root? Or something else? + let path = FilePath(str) + guard path.root == nil, path.components.count == 1 else { + return nil + } + self = path.components.first! + self._invariantCheck() + } +} + +@available(System 0.0.2, *) +extension FilePath.Root { + internal init?(_ str: SystemString) { + // FIXME: explicit null root? Or something else? + let path = FilePath(str) + guard path.root != nil, path.components.isEmpty else { + return nil + } + self = path.root! + self._invariantCheck() + } +} + +// MARK: - Invariants + +@available(System 0.0.2, *) +extension FilePath.Component { + // TODO: ensure this all gets easily optimized away in release... + internal func _invariantCheck() { + #if DEBUG + precondition(!_slice.isEmpty) + precondition(_slice.last != .null) + precondition(_slice.allSatisfy { !isSeparator($0) } ) + precondition(_path._relativeStart <= _slice.startIndex) + #endif // DEBUG + } +} + +@available(System 0.0.2, *) +extension FilePath.Root { + internal func _invariantCheck() { + #if DEBUG + precondition(self._rootEnd > _path._storage.startIndex) + + // TODO: Windows root invariants + #endif + } +} diff --git a/Sources/System/FilePath/FilePathParsing.swift b/Sources/System/FilePath/FilePathParsing.swift new file mode 100644 index 00000000..f372dd67 --- /dev/null +++ b/Sources/System/FilePath/FilePathParsing.swift @@ -0,0 +1,392 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +// FIXME: Need to rewrite and simplify this code now that SystemString +// manages (and hides) the null terminator + +// The separator we use internally +private var genericSeparator: SystemChar { .slash } + +// The platform preferred separator +// +// TODO: Make private +internal var platformSeparator: SystemChar { + _windowsPaths ? .backslash : genericSeparator +} + +// Whether the character is the canonical separator +// TODO: Make private +internal func isSeparator(_ c: SystemChar) -> Bool { + c == platformSeparator +} + +// Whether the character is a pre-normalized separator +internal func isPrenormalSeparator(_ c: SystemChar) -> Bool { + c == genericSeparator || c == platformSeparator +} + +// Separator normalization, checking, and root parsing is internally hosted +// on SystemString for ease of unit testing. + +extension SystemString { + // For invariant enforcing/checking. Should always return false on + // a fully-formed path + fileprivate func _hasTrailingSeparator() -> Bool { + // Just a root: do nothing + guard _relativePathStart != endIndex else { return false } + assert(!isEmpty) + + return isSeparator(self.last!) + } + + // Enforce invariants by removing a trailing separator. + // + // Precondition: There is exactly zero or one trailing slashes + // + // Postcondition: Path is root, or has no trailing separator + internal mutating func _removeTrailingSeparator() { + if _hasTrailingSeparator() { + self.removeLast() + assert(!_hasTrailingSeparator()) + } + } + + // Enforce invariants by normalizing the internal separator representation. + // + // 1) Normalize all separators to platform-preferred separator + // 2) Drop redundant separators + // 3) Drop trailing separators + // + // On Windows, UNC and device paths are allowed to begin with two separators, + // and partial or mal-formed roots are completed. + // + // The POSIX standard does allow two leading separators to + // denote implementation-specific handling, but Darwin and Linux + // do not treat these differently. + // + internal mutating func _normalizeSeparators() { + guard !isEmpty else { return } + var (writeIdx, readIdx) = (startIndex, startIndex) + + if _windowsPaths { + // Normalize forwards slashes to backslashes. + // + // NOTE: Ideally this would be done as part of separator coalescing + // below. However, prenormalizing roots such as UNC paths requires + // parsing and (potentially) fixing up semi-formed roots. This + // normalization reduces the complexity of the task by allowing us to + // use a read-only lexer. + self._replaceAll(genericSeparator, with: platformSeparator) + + // Windows roots can have meaningful repeated backslashes or may + // need backslashes inserted for partially-formed roots. Delegate that to + // `_prenormalizeWindowsRoots` and resume. + readIdx = _prenormalizeWindowsRoots() + writeIdx = readIdx + + // Skip redundant separators + while readIdx < endIndex && isSeparator(self[readIdx]) { + self.formIndex(after: &readIdx) + } + } else { + assert(genericSeparator == platformSeparator) + } + + while readIdx < endIndex { + assert(writeIdx <= readIdx) + + // Swap and advance our indices. + let wasSeparator = isSeparator(self[readIdx]) + self.swapAt(writeIdx, readIdx) + self.formIndex(after: &writeIdx) + self.formIndex(after: &readIdx) + + while wasSeparator, readIdx < endIndex, isSeparator(self[readIdx]) { + self.formIndex(after: &readIdx) + } + } + self.removeLast(self.distance(from: writeIdx, to: readIdx)) + self._removeTrailingSeparator() + } +} + +@available(System 0.0.1, *) +extension FilePath { + internal mutating func _removeTrailingSeparator() { + _storage._removeTrailingSeparator() + } + + internal mutating func _normalizeSeparators() { + _storage._normalizeSeparators() + } + + // Remove any `.` and `..` components + internal mutating func _normalizeSpecialDirectories() { + guard !isLexicallyNormal else { return } + defer { assert(isLexicallyNormal) } + + let relStart = _relativeStart + let hasRoot = relStart != _storage.startIndex + + // TODO: all this logic might be nicer if _parseComponent considered + // the null character index to be the next start... + + var (writeIdx, readIdx) = (relStart, relStart) + while readIdx < _storage.endIndex { + let (compEnd, nextStart) = _parseComponent(startingAt: readIdx) + assert(readIdx < nextStart && compEnd <= nextStart) + let component = readIdx..= writeIdx) + if readIdx != writeIdx { + _storage.removeSubrange(writeIdx...) + _removeTrailingSeparator() + } + } +} + +extension SystemString { + internal var _relativePathStart: Index { + _parseRoot().relativeBegin + } +} + +@available(System 0.0.1, *) +extension FilePath { + internal var _relativeStart: SystemString.Index { + _storage._relativePathStart + } + internal var _hasRoot: Bool { + _relativeStart != _storage.startIndex + } +} + +// Parse separators + +@available(System 0.0.1, *) +extension FilePath { + internal typealias _Index = SystemString.Index + + // Parse a component that starts at `i`. Returns the end + // of the component and the start of the next. Parsing terminates + // at the index of the null byte. + internal func _parseComponent( + startingAt i: _Index + ) -> (componentEnd: _Index, nextStart: _Index) { + assert(i < _storage.endIndex) + // Parse the root + if i == _storage.startIndex { + let relativeStart = _relativeStart + if i != relativeStart { + return (relativeStart, relativeStart) + } + } + + assert(!isSeparator(_storage[i])) + guard let nextSep = _storage[i...].firstIndex(where: isSeparator) else { + return (_storage.endIndex, _storage.endIndex) + } + return (nextSep, _storage.index(after: nextSep)) + } + + // Parse a component prior to the one that starts at `i`. Returns + // the start of the prior component. If `i` is the index of null, + // returns the last component. + internal func _parseComponent( + priorTo i: _Index + ) -> Range<_Index> { + precondition(i > _storage.startIndex) + let relStart = _relativeStart + + if i == relStart { return _storage.startIndex.. relStart) + + var slice = _storage[..) -> Bool { + _storage[component].elementsEqual([.dot]) + } + + internal func _isParentDirectory(_ component: Range<_Index>) -> Bool { + _storage[component].elementsEqual([.dot, .dot]) + } + + internal func _isSpecialDirectory(_ component: Range<_Index>) -> Bool { + _isCurrentDirectory(component) || _isParentDirectory(component) + } +} + +@available(System 0.0.2, *) +extension FilePath.ComponentView { + // TODO: Store this... + internal var _relativeStart: SystemString.Index { + _path._relativeStart + } +} + +extension SystemString { + internal func _parseRoot() -> ( + rootEnd: Index, relativeBegin: Index + ) { + guard !isEmpty else { return (startIndex, startIndex) } + + // Windows roots are more complex + if _windowsPaths { return _parseWindowsRoot() } + + // A leading `/` is a root + guard isSeparator(self.first!) else { return (startIndex, startIndex) } + + let next = self.index(after: startIndex) + return (next, next) + } +} + +@available(System 0.0.2, *) +extension FilePath.Root { + // Asserting self is a root, returns whether this is an + // absolute root. + // + // On Unix, all roots are absolute. On Windows, `\` and `X:` are + // relative roots + // + // TODO: public + internal var isAbsolute: Bool { + assert(FilePath(SystemString(self._slice)).root == self, "not a root") + + guard _windowsPaths else { return true } + + // `\` or `C:` are the only form of relative roots, and all + // absolute roots are at least 3 chars long. + let slice = self._slice + guard slice.count < 3 else { return true } + assert( + (slice.count == 1 && slice.first == .backslash) || + (slice.count == 2 && slice.last == .colon)) + return false + } +} + +@available(System 0.0.1, *) +extension FilePath { + internal var _portableDescription: String { + guard _windowsPaths else { return description } + let utf8 = description.utf8.map { $0 == UInt8(ascii: #"\"#) ? UInt8(ascii: "/") : $0 } + return String(decoding: utf8, as: UTF8.self) + } +} + +// Whether we are providing Windows paths +@inline(__always) +internal var _windowsPaths: Bool { + if let forceWindowsPaths = forceWindowsPaths { + return forceWindowsPaths + } + #if os(Windows) + return true + #else + return false + #endif +} + +@available(System 0.0.1, *) +extension FilePath { + // Whether we should add a separator when doing an append + internal var _needsSeparatorForAppend: Bool { + guard let last = _storage.last, !isSeparator(last) else { return false } + + // On Windows, we can have a path of the form `C:` which is a root and + // does not need a separator after it + if _windowsPaths && _relativeStart == _storage.endIndex { + return false + } + + return true + } + + // Perform an append, inseting a separator if needed. + // Note that this will not check whether `content` is a root + internal mutating func _append(unchecked content: Slice) { + assert(FilePath(SystemString(content)).root == nil) + if content.isEmpty { return } + if _needsSeparatorForAppend { + _storage.append(platformSeparator) + } + _storage.append(contentsOf: content) + } +} + +// MARK: - Invariants +@available(System 0.0.1, *) +extension FilePath { + internal func _invariantsSatisfied() -> Bool { + var normal = self + normal._normalizeSeparators() + guard self == normal else { return false } + guard !self._storage._hasTrailingSeparator() else { return false } + guard _hasRoot == (self.root != nil) else { return false } + return true + } + + internal func _invariantCheck() { + #if DEBUG + precondition(_invariantsSatisfied()) + #endif // DEBUG + } +} diff --git a/Sources/System/FilePath/FilePathString.swift b/Sources/System/FilePath/FilePathString.swift new file mode 100644 index 00000000..45f79c8a --- /dev/null +++ b/Sources/System/FilePath/FilePathString.swift @@ -0,0 +1,603 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +// MARK: - Platform string + +@available(System 0.0.2, *) +extension FilePath { + /// Creates a file path by copying bytes from a null-terminated platform + /// string. + /// + /// - Parameter platformString: A pointer to a null-terminated platform + /// string. + public init(platformString: UnsafePointer) { + self.init(_platformString: platformString) + } + + /// Creates a file path by copying bytes from a null-terminated platform + /// string. + /// + /// - Note It is a precondition that `platformString` must be null-terminated. + /// The absence of a null byte will trigger a runtime error. + /// + /// - Parameter platformString: A null-terminated platform string. + @inlinable + @_alwaysEmitIntoClient + public init(platformString: [CInterop.PlatformChar]) { + guard let _ = platformString.firstIndex(of: 0) else { + fatalError( + "input of FilePath.init(platformString:) must be null-terminated" + ) + } + self = platformString.withUnsafeBufferPointer { + FilePath(platformString: $0.baseAddress!) + } + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use FilePath.init(_ scalar: Unicode.Scalar)") + public init(platformString: inout CInterop.PlatformChar) { + guard platformString == 0 else { + fatalError( + "input of FilePath.init(platformString:) must be null-terminated" + ) + } + self = FilePath() + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use FilePath(_: String) to create a path from a String") + public init(platformString: String) { + if let nullLoc = platformString.firstIndex(of: "\0") { + self = FilePath(String(platformString[..( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + return try withCString(body) + } +#else + /// Calls the given closure with a pointer to the contents of the file path, + /// represented as a null-terminated platform string. + /// + /// - Parameter body: A closure with a pointer parameter + /// that points to a null-terminated platform string. + /// If `body` has a return value, + /// that value is also used as the return value for this method. + /// - Returns: The return value, if any, of the `body` closure parameter. + /// + /// The pointer passed as an argument to `body` is valid + /// only during the execution of this method. + /// Don't try to store the pointer for later use. + public func withPlatformString( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + return try _withPlatformString(body) + } +#endif +} + +@available(System 0.0.2, *) +extension FilePath.Component { + /// Creates a file path component by copying bytes from a null-terminated + /// platform string. + /// + /// Returns `nil` if `platformString` is empty, is a root, or has more than + /// one component in it. + /// + /// - Parameter platformString: A pointer to a null-terminated platform + /// string. + public init?(platformString: UnsafePointer) { + self.init(_platformString: platformString) + } + + /// Creates a file path component by copying bytes from a null-terminated + /// platform string. It is a precondition that a null byte indicates the end of + /// the string. The absence of a null byte will trigger a runtime error. + /// + /// Returns `nil` if `platformString` is empty, is a root, or has more than + /// one component in it. + /// + /// - Note It is a precondition that `platformString` must be null-terminated. + /// The absence of a null byte will trigger a runtime error. + /// + /// - Parameter platformString: A null-terminated platform string. + @inlinable + @_alwaysEmitIntoClient + public init?(platformString: [CInterop.PlatformChar]) { + guard let _ = platformString.firstIndex(of: 0) else { + fatalError( + "input of FilePath.Component.init?(platformString:) must be null-terminated" + ) + } + guard let component = platformString.withUnsafeBufferPointer({ + FilePath.Component(platformString: $0.baseAddress!) + }) else { + return nil + } + self = component + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use FilePath.Component.init(_ scalar: Unicode.Scalar)") + public init?(platformString: inout CInterop.PlatformChar) { + guard platformString == 0 else { + fatalError( + "input of FilePath.Component.init?(platformString:) must be null-terminated" + ) + } + return nil + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use FilePath.Component.init(_: String)") + public init?(platformString: String) { + let string: String + if let nullLoc = platformString.firstIndex(of: "\0") { + string = String(platformString[..( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + try _withPlatformString(body) + } +} + +@available(System 0.0.2, *) +extension FilePath.Root { + /// Creates a file path root by copying bytes from a null-terminated platform + /// string. + /// + /// Returns `nil` if `platformString` is empty or is not a root. + /// + /// - Parameter platformString: A pointer to a null-terminated platform + /// string. + public init?(platformString: UnsafePointer) { + self.init(_platformString: platformString) + } + + /// Creates a file path root by copying bytes from a null-terminated platform + /// string. It is a precondition that a null byte indicates the end of + /// the string. The absence of a null byte will trigger a runtime error. + /// + /// Returns `nil` if `platformString` is empty or is not a root. + /// + /// - Note It is a precondition that `platformString` must be null-terminated. + /// The absence of a null byte will trigger a runtime error. + /// + /// - Parameter platformString: A null-terminated platform string. + @inlinable + @_alwaysEmitIntoClient + public init?(platformString: [CInterop.PlatformChar]) { + guard let _ = platformString.firstIndex(of: 0) else { + fatalError( + "input of FilePath.Root.init?(platformString:) must be null-terminated" + ) + } + guard let component = platformString.withUnsafeBufferPointer({ + FilePath.Root(platformString: $0.baseAddress!) + }) else { + return nil + } + self = component + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use FilePath.Root.init(_ scalar: Unicode.Scalar)") + public init?(platformString: inout CInterop.PlatformChar) { + guard platformString == 0 else { + fatalError( + "input of FilePath.Root.init?(platformString:) must be null-terminated" + ) + } + return nil + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use FilePath.Root.init(_: String)") + public init?(platformString: String) { + let string: String + if let nullLoc = platformString.firstIndex(of: "\0") { + string = String(platformString[..( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + try _withPlatformString(body) + } +} + +// MARK: - String literals + +@available(System 0.0.1, *) +extension FilePath: ExpressibleByStringLiteral { + /// Creates a file path from a string literal. + /// + /// - Parameter stringLiteral: A string literal + /// whose Unicode encoded contents to use as the contents of the path. + public init(stringLiteral: String) { + self.init(stringLiteral) + } + + /// Creates a file path from a string. + /// + /// - Parameter string: A string + /// whose Unicode encoded contents to use as the contents of the path. + public init(_ string: String) { + self.init(SystemString(string)) + } +} + +@available(System 0.0.2, *) +extension FilePath.Component: ExpressibleByStringLiteral { + /// Create a file path component from a string literal. + /// + /// Precondition: `stringLiteral` is non-empty, is not a root, + /// and has only one component in it. + public init(stringLiteral: String) { + guard let s = FilePath.Component(stringLiteral) else { + // TODO: static assert + fatalError(""" + FilePath.Component must be created from exactly one non-root component + """) + } + self = s + } + + /// Create a file path component from a string. + /// + /// Returns `nil` if `string` is empty, a root, or has more than one component + /// in it. + public init?(_ string: String) { + self.init(SystemString(string)) + } +} + +@available(System 0.0.2, *) +extension FilePath.Root: ExpressibleByStringLiteral { + /// Create a file path root from a string literal. + /// + /// Precondition: `stringLiteral` is non-empty and is a root. + public init(stringLiteral: String) { + guard let s = FilePath.Root(stringLiteral) else { + // TODO: static assert + fatalError(""" + FilePath.Root must be created from a root + """) + } + self = s + } + + /// Create a file path root from a string. + /// + /// Returns `nil` if `string` is empty or is not a root. + public init?(_ string: String) { + self.init(SystemString(string)) + } +} + +// MARK: - Printing and dumping + +@available(System 0.0.1, *) +extension FilePath: CustomStringConvertible, CustomDebugStringConvertible { + /// A textual representation of the file path. + /// + /// If the content of the path isn't a well-formed Unicode string, + /// this replaces invalid bytes with U+FFFD. See `String.init(decoding:)` + @inline(never) + public var description: String { String(decoding: self) } + + /// A textual representation of the file path, suitable for debugging. + /// + /// If the content of the path isn't a well-formed Unicode string, + /// this replaces invalid bytes with U+FFFD. See `String.init(decoding:)` + public var debugDescription: String { description.debugDescription } +} + +@available(System 0.0.2, *) +extension FilePath.Component: CustomStringConvertible, CustomDebugStringConvertible { + + /// A textual representation of the path component. + /// + /// If the content of the path component isn't a well-formed Unicode string, + /// this replaces invalid bytes with U+FFFD. See `String.init(decoding:)`. + @inline(never) + public var description: String { String(decoding: self) } + + /// A textual representation of the path component, suitable for debugging. + /// + /// If the content of the path component isn't a well-formed Unicode string, + /// this replaces invalid bytes with U+FFFD. See `String.init(decoding:)`. + public var debugDescription: String { description.debugDescription } +} + +@available(System 0.0.2, *) +extension FilePath.Root: CustomStringConvertible, CustomDebugStringConvertible { + + /// A textual representation of the path root. + /// + /// If the content of the path root isn't a well-formed Unicode string, + /// this replaces invalid bytes with U+FFFD. See `String.init(decoding:)`. + @inline(never) + public var description: String { String(decoding: self) } + + /// A textual representation of the path root, suitable for debugging. + /// + /// If the content of the path root isn't a well-formed Unicode string, + /// this replaces invalid bytes with U+FFFD. See `String.init(decoding:)`. + public var debugDescription: String { description.debugDescription } +} + +// MARK: - Convenience helpers + +// Convenience helpers +@available(System 0.0.2, *) +extension FilePath { + /// Creates a string by interpreting the path’s content as UTF-8 on Unix + /// and UTF-16 on Windows. + /// + /// This property is equivalent to calling `String(decoding: path)` + public var string: String { + String(decoding: self) + } +} + +@available(System 0.0.2, *) +extension FilePath.Component { + /// Creates a string by interpreting the component’s content as UTF-8 on Unix + /// and UTF-16 on Windows. + /// + /// This property is equivalent to calling `String(decoding: component)`. + public var string: String { + String(decoding: self) + } +} + +@available(System 0.0.2, *) +extension FilePath.Root { + /// On Unix, this returns `"/"`. + /// + /// On Windows, interprets the root's content as UTF-16 on Windows. + /// + /// This property is equivalent to calling `String(decoding: root)`. + public var string: String { + String(decoding: self) + } +} + +// MARK: - Decoding and validating + +@available(System 0.0.1, *) +extension String { + /// Creates a string by interpreting the file path's content as UTF-8 on Unix + /// and UTF-16 on Windows. + /// + /// - Parameter path: The file path to be interpreted as + /// `CInterop.PlatformUnicodeEncoding`. + /// + /// If the content of the file path isn't a well-formed Unicode string, + /// this initializer replaces invalid bytes with U+FFFD. + /// This means that, depending on the semantics of the specific file system, + /// conversion to a string and back to a path + /// might result in a value that's different from the original path. + public init(decoding path: FilePath) { + self.init(_decoding: path) + } + + /// Creates a string from a file path, validating its contents as UTF-8 on + /// Unix and UTF-16 on Windows. + /// + /// - Parameter path: The file path to be interpreted as + /// `CInterop.PlatformUnicodeEncoding`. + /// + /// If the contents of the file path isn't a well-formed Unicode string, + /// this initializer returns `nil`. + public init?(validating path: FilePath) { + self.init(_validating: path) + } +} + +@available(System 0.0.2, *) +extension String { + /// Creates a string by interpreting the path component's content as UTF-8 on + /// Unix and UTF-16 on Windows. + /// + /// - Parameter component: The path component to be interpreted as + /// `CInterop.PlatformUnicodeEncoding`. + /// + /// If the content of the path component isn't a well-formed Unicode string, + /// this initializer replaces invalid bytes with U+FFFD. + /// This means that, depending on the semantics of the specific file system, + /// conversion to a string and back to a path component + /// might result in a value that's different from the original path component. + public init(decoding component: FilePath.Component) { + self.init(_decoding: component) + } + + /// Creates a string from a path component, validating its contents as UTF-8 + /// on Unix and UTF-16 on Windows. + /// + /// - Parameter component: The path component to be interpreted as + /// `CInterop.PlatformUnicodeEncoding`. + /// + /// If the contents of the path component isn't a well-formed Unicode string, + /// this initializer returns `nil`. + public init?(validating component: FilePath.Component) { + self.init(_validating: component) + } +} + +@available(System 0.0.2, *) +extension String { + /// On Unix, creates the string `"/"` + /// + /// On Windows, creates a string by interpreting the path root's content as + /// UTF-16. + /// + /// - Parameter root: The path root to be interpreted as + /// `CInterop.PlatformUnicodeEncoding`. + /// + /// If the content of the path root isn't a well-formed Unicode string, + /// this initializer replaces invalid bytes with U+FFFD. + /// This means that on Windows, + /// conversion to a string and back to a path root + /// might result in a value that's different from the original path root. + public init(decoding root: FilePath.Root) { + self.init(_decoding: root) + } + + /// On Unix, creates the string `"/"` + /// + /// On Windows, creates a string from a path root, validating its contents as + /// UTF-16 on Windows. + /// + /// - Parameter root: The path root to be interpreted as + /// `CInterop.PlatformUnicodeEncoding`. + /// + /// On Windows, if the contents of the path root isn't a well-formed Unicode + /// string, this initializer returns `nil`. + public init?(validating root: FilePath.Root) { + self.init(_validating: root) + } +} + +// MARK: - Internal helpers + +extension String { + fileprivate init(_decoding ps: PS) { + self = ps._withPlatformString { String(platformString: $0) } + } + + fileprivate init?(_validating ps: PS) { + guard let str = ps._withPlatformString( + String.init(validatingPlatformString:) + ) else { + return nil + } + self = str + } +} + +// MARK: - Deprecations + +@available(System 0.0.1, *) +extension String { + @available(*, deprecated, renamed: "init(decoding:)") + public init(_ path: FilePath) { self.init(decoding: path) } + + @available(*, deprecated, renamed: "init(validating:)") + public init?(validatingUTF8 path: FilePath) { self.init(validating: path) } +} + +#if !os(Windows) +@available(System 0.0.1, *) +extension FilePath { + /// For backwards compatibility only. This initializer is equivalent to + /// the preferred `FilePath(platformString:)`. + @available(*, deprecated, renamed: "init(platformString:)") + public init(cString: UnsafePointer) { + self.init(platformString: cString) + } + + @available(*, deprecated, renamed: "init(platformString:)") + public init(cString: [CChar]) { + self.init(platformString: cString) + } + + @available(*, deprecated, renamed: "init(platformString:)") + public init(cString: inout CChar) { + self.init(platformString: &cString) + } + + @available(*, deprecated, renamed: "init(platformString:)") + public init(cString: String) { + self.init(platformString: cString) + } + + /// For backwards compatibility only. This function is equivalent to + /// the preferred `withPlatformString`. + public func withCString( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + return try _withPlatformString(body) + } +} +#endif diff --git a/Sources/System/FilePath/FilePathSyntax.swift b/Sources/System/FilePath/FilePathSyntax.swift new file mode 100644 index 00000000..eb1f25df --- /dev/null +++ b/Sources/System/FilePath/FilePathSyntax.swift @@ -0,0 +1,593 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +// MARK: - Query API + +@available(System 0.0.2, *) +extension FilePath { + /// Returns true if this path uniquely identifies the location of + /// a file without reference to an additional starting location. + /// + /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is + /// equivalent to `root != nil`. + /// + /// On Windows, absolute paths are fully qualified paths. `isAbsolute` is + /// _not_ equivalent to `root != nil` for traditional DOS paths + /// (e.g. `C:foo` and `\bar` have roots but are not absolute). UNC paths + /// and device paths are always absolute. Traditional DOS paths are + /// absolute only if they begin with a volume or drive followed by + /// a `:` and a separator. + /// + /// NOTE: This does not perform shell expansion or substitute + /// environment variables; paths beginning with `~` are considered relative. + /// + /// Examples: + /// * Unix: + /// * `/usr/local/bin` + /// * `/tmp/foo.txt` + /// * `/` + /// * Windows: + /// * `C:\Users\` + /// * `\\?\UNC\server\share\bar.exe` + /// * `\\server\share\bar.exe` + public var isAbsolute: Bool { + self.root?.isAbsolute ?? false + } + + /// Returns true if this path is not absolute (see `isAbsolute`). + /// + /// Examples: + /// * Unix: + /// * `~/bar` + /// * `tmp/foo.txt` + /// * Windows: + /// * `bar\baz` + /// * `C:Users\` + /// * `\Users` + public var isRelative: Bool { !isAbsolute } + + // TODO(Windows docs): examples with roots, such as whether `\foo\bar` + // starts with `C:\foo` + /// Returns whether `other` is a prefix of `self`, only considering + /// whole path components. + /// + /// Example: + /// + /// let path: FilePath = "/usr/bin/ls" + /// path.starts(with: "/") // true + /// path.starts(with: "/usr/bin") // true + /// path.starts(with: "/usr/bin/ls") // true + /// path.starts(with: "/usr/bin/ls///") // true + /// path.starts(with: "/us") // false + public func starts(with other: FilePath) -> Bool { + guard !other.isEmpty else { return true } + return self.root == other.root && components.starts( + with: other.components) + } + + // TODO(Windows docs): examples with roots, such as whether `C:\foo\bar` + // ends with `C:bar` + /// Returns whether `other` is a suffix of `self`, only considering + /// whole path components. + /// + /// Example: + /// + /// let path: FilePath = "/usr/bin/ls" + /// path.ends(with: "ls") // true + /// path.ends(with: "bin/ls") // true + /// path.ends(with: "usr/bin/ls") // true + /// path.ends(with: "/usr/bin/ls///") // true + /// path.ends(with: "/ls") // false + public func ends(with other: FilePath) -> Bool { + if other.root != nil { + // TODO: anything tricky here for Windows? + return self == other + } + + return components.reversed().starts( + with: other.components.reversed()) + } + + /// Whether this path is empty + public var isEmpty: Bool { _storage.isEmpty } +} + +// MARK: - Decompose a path +@available(System 0.0.2, *) +extension FilePath { + /// Returns the root of a path if there is one, otherwise `nil`. + /// + /// On Unix, this will return the leading `/` if the path is absolute + /// and `nil` if the path is relative. + /// + /// On Windows, for traditional DOS paths, this will return + /// the path prefix up to and including a root directory or + /// a supplied drive or volume. Otherwise, if the path is relative to + /// both the current directory and current drive, returns `nil`. + /// + /// On Windows, for UNC or device paths, this will return the path prefix + /// up to and including the host and share for UNC paths or the volume for + /// device paths followed by any subsequent separator. + /// + /// Examples: + /// * Unix: + /// * `/foo/bar => /` + /// * `foo/bar => nil` + /// * Windows: + /// * `C:\foo\bar => C:\` + /// * `C:foo\bar => C:` + /// * `\foo\bar => \ ` + /// * `foo\bar => nil` + /// * `\\server\share\file => \\server\share\` + /// * `\\?\UNC\server\share\file => \\?\UNC\server\share\` + /// * `\\.\device\folder => \\.\device\` + /// + /// Setting the root to `nil` will remove the root and setting a new + /// root will replace the root. + /// + /// Example: + /// + /// var path: FilePath = "/foo/bar" + /// path.root = nil // path is "foo/bar" + /// path.root = "/" // path is "/foo/bar" + /// + /// Example (Windows): + /// + /// var path: FilePath = #"\foo\bar"# + /// path.root = nil // path is #"foo\bar"# + /// path.root = "C:" // path is #"C:foo\bar"# + /// path.root = #"C:\"# // path is #"C:\foo\bar"# + public var root: FilePath.Root? { + get { + guard _hasRoot else { return nil } + return Root(self, rootEnd: _relativeStart) + } + set { + defer { _invariantCheck() } + guard let r = newValue else { + _storage.removeSubrange(..<_relativeStart) + return + } + _storage.replaceSubrange(..<_relativeStart, with: r._slice) + } + } + + /// Creates a new path containing just the components, i.e. everything + /// after `root`. + /// + /// Returns self if `root == nil`. + /// + /// Examples: + /// * Unix: + /// * `/foo/bar => foo/bar` + /// * `foo/bar => foo/bar` + /// * `/ => ""` + /// * Windows: + /// * `C:\foo\bar => foo\bar` + /// * `foo\bar => foo\bar` + /// * `\\?\UNC\server\share\file => file` + /// * `\\?\device\folder\file.exe => folder\file.exe` + /// * `\\server\share\file => file` + /// * `\ => ""` + public __consuming func removingRoot() -> FilePath { + var copy = self + copy.root = nil + return copy + } +} + +@available(System 0.0.2, *) +extension FilePath { + /// Returns the final component of the path. + /// Returns `nil` if the path is empty or only contains a root. + /// + /// Note: Even if the final component is a special directory + /// (`.` or `..`), it will still be returned. See `lexicallyNormalize()`. + /// + /// Examples: + /// * Unix: + /// * `/usr/local/bin/ => bin` + /// * `/tmp/foo.txt => foo.txt` + /// * `/tmp/foo.txt/.. => ..` + /// * `/tmp/foo.txt/. => .` + /// * `/ => nil` + /// * Windows: + /// * `C:\Users\ => Users` + /// * `C:Users\ => Users` + /// * `C:\ => nil` + /// * `\Users\ => Users` + /// * `\\?\UNC\server\share\bar.exe => bar.exe` + /// * `\\server\share => nil` + /// * `\\?\UNC\server\share\ => nil` + public var lastComponent: Component? { components.last } + + /// Creates a new path with everything up to but not including + /// `lastComponent`. + /// + /// If the path only contains a root, returns `self`. + /// If the path has no root and only includes a single component, + /// returns an empty FilePath. + /// + /// Examples: + /// * Unix: + /// * `/usr/bin/ls => /usr/bin` + /// * `/foo => /` + /// * `/ => /` + /// * `foo => ""` + /// * Windows: + /// * `C:\foo\bar.exe => C:\foo` + /// * `C:\ => C:\` + /// * `\\server\share\folder\file.txt => \\server\share\folder` + /// * `\\server\share\ => \\server\share\` + public __consuming func removingLastComponent() -> FilePath { + var copy = self + copy.removeLastComponent() + return copy + } + + /// In-place mutating variant of `removingLastComponent`. + /// + /// If `self` only contains a root, does nothing and returns `false`. + /// Otherwise removes `lastComponent` and returns `true`. + /// + /// Example: + /// + /// var path = "/usr/bin" + /// path.removeLastComponent() == true // path is "/usr" + /// path.removeLastComponent() == true // path is "/" + /// path.removeLastComponent() == false // path is "/" + @discardableResult + public mutating func removeLastComponent() -> Bool { + defer { _invariantCheck() } + guard let lastRel = lastComponent else { return false } + _storage.removeSubrange(lastRel._slice.indices) + _removeTrailingSeparator() + return true + } +} + +@available(System 0.0.2, *) +extension FilePath.Component { + /// The extension of this file or directory component. + /// + /// If `self` does not contain a `.` anywhere, or only + /// at the start, returns `nil`. Otherwise, returns everything after the dot. + /// + /// Examples: + /// * `foo.txt => txt` + /// * `foo.tar.gz => gz` + /// * `Foo.app => app` + /// * `.hidden => nil` + /// * `.. => nil` + public var `extension`: String? { + guard let range = _extensionRange() else { return nil } + return _slice[range].string + } + + /// The non-extension portion of this file or directory component. + /// + /// Examples: + /// * `foo.txt => foo` + /// * `foo.tar.gz => foo.tar` + /// * `Foo.app => Foo` + /// * `.hidden => .hidden` + /// * `.. => ..` + public var stem: String { + _slice[_stemRange()].string + } +} + +@available(System 0.0.2, *) +extension FilePath { + + /// The extension of the file or directory last component. + /// + /// If `lastComponent` is `nil` or one of the special path components + /// `.` or `..`, `get` returns `nil` and `set` does nothing. + /// + /// If `lastComponent` does not contain a `.` anywhere, or only + /// at the start, `get` returns `nil` and `set` will append a + /// `.` and `newValue` to `lastComponent`. + /// + /// Otherwise `get` returns everything after the last `.` and `set` will + /// replace the extension. + /// + /// Examples: + /// * `/tmp/foo.txt => txt` + /// * `/Applications/Foo.app/ => app` + /// * `/Applications/Foo.app/bar.txt => txt` + /// * `/tmp/foo.tar.gz => gz` + /// * `/tmp/.hidden => nil` + /// * `/tmp/.hidden. => ""` + /// * `/tmp/.. => nil` + /// + /// Example: + /// + /// var path = "/tmp/file" + /// path.extension = "txt" // path is "/tmp/file.txt" + /// path.extension = "o" // path is "/tmp/file.o" + /// path.extension = nil // path is "/tmp/file" + /// path.extension = "" // path is "/tmp/file." + public var `extension`: String? { + get { lastComponent?.extension } + set { + defer { _invariantCheck() } + guard let base = lastComponent, base.kind == .regular else { return } + + let suffix: SystemString + if let ext = newValue { + suffix = _makeExtension(ext) + } else { + suffix = SystemString() + } + + let extRange = ( + base._extensionIndex() ?? base._slice.endIndex + ) ..< base._slice.endIndex + + _storage.replaceSubrange(extRange, with: suffix) + } + } + + /// The non-extension portion of the file or directory last component. + /// + /// Returns `nil` if `lastComponent` is `nil` + /// + /// * `/tmp/foo.txt => foo` + /// * `/Applications/Foo.app/ => Foo` + /// * `/Applications/Foo.app/bar.txt => bar` + /// * `/tmp/.hidden => .hidden` + /// * `/tmp/.. => ..` + /// * `/ => nil` + public var stem: String? { lastComponent?.stem } + +} + +@available(System 0.0.2, *) +extension FilePath { + /// Whether the path is in lexical-normal form, that is `.` and `..` + /// components have been collapsed lexically (i.e. without following + /// symlinks). + /// + /// Examples: + /// * `"/usr/local/bin".isLexicallyNormal == true` + /// * `"../local/bin".isLexicallyNormal == true` + /// * `"local/bin/..".isLexicallyNormal == false` + public var isLexicallyNormal: Bool { + // `..` components are permitted at the front of a + // relative path, otherwise there should be no special directories + // + // FIXME: Windows `C:..\foo\bar` should probably be lexically normal, but + // `\..\foo\bar` should not. + components.drop( + while: { root == nil && $0.kind == .parentDirectory } + ).allSatisfy { $0.kind == .regular } + } + + /// Collapse `.` and `..` components lexically (i.e. without following + /// symlinks). + /// + /// Examples: + /// * `/usr/./local/bin/.. => /usr/local` + /// * `/../usr/local/bin => /usr/local/bin` + /// * `../usr/local/../bin => ../usr/bin` + public mutating func lexicallyNormalize() { + defer { _invariantCheck() } + _normalizeSpecialDirectories() + } + + /// Returns a copy of `self` in lexical-normal form, that is `.` and `..` + /// components have been collapsed lexically (i.e. without following + /// symlinks). See `lexicallyNormalize` + public __consuming func lexicallyNormalized() -> FilePath { + var copy = self + copy.lexicallyNormalize() + return copy + } + + /// Create a new `FilePath` by resolving `subpath` relative to `self`, + /// ensuring that the result is lexically contained within `self`. + /// + /// `subpath` will be lexically normalized (see `lexicallyNormalize`) as + /// part of resolution, meaning any contained `.` and `..` components will + /// be collapsed without resolving symlinks. Any root in `subpath` will be + /// ignored. + /// + /// Returns `nil` if the result would "escape" from `self` through use of + /// the special directory component `..`. + /// + /// This is useful for protecting against arbitrary path traversal from an + /// untrusted subpath: the result is guaranteed to be lexically contained + /// within `self`. Since this operation does not consult the file system to + /// resolve symlinks, any escaping symlinks nested inside of `self` can still + /// be targeted by the result. + /// + /// Example: + /// + /// let staticContent: FilePath = "/var/www/my-website/static" + /// let links: [FilePath] = + /// ["index.html", "/assets/main.css", "../../../../etc/passwd"] + /// links.map { staticContent.lexicallyResolving($0) } + /// // ["/var/www/my-website/static/index.html", + /// // "/var/www/my-website/static/assets/main.css", + /// // nil] + public __consuming func lexicallyResolving( + _ subpath: __owned FilePath + ) -> FilePath? { + let subpath = subpath.removingRoot().lexicallyNormalized() + guard !subpath.isEmpty else { return self } + guard subpath.components.first?.kind != .parentDirectory else { + return nil + } + return self.appending(subpath.components) + } +} + +// Modification and concatenation API +@available(System 0.0.2, *) +extension FilePath { + // TODO(Windows docs): example with roots + /// If `prefix` is a prefix of `self`, removes it and returns `true`. + /// Otherwise returns `false`. + /// + /// Example: + /// + /// var path: FilePath = "/usr/local/bin" + /// path.removePrefix("/usr/bin") // false + /// path.removePrefix("/us") // false + /// path.removePrefix("/usr/local") // true, path is "bin" + public mutating func removePrefix(_ prefix: FilePath) -> Bool { + defer { _invariantCheck() } + // FIXME: Should Windows have more nuanced semantics? + guard root == prefix.root else { return false } + let (tail, remainder) = _dropCommonPrefix(components, prefix.components) + guard remainder.isEmpty else { return false } + self._storage.removeSubrange(..( + _ components: __owned C + ) where C.Element == FilePath.Component { + defer { _invariantCheck() } + for c in components { + _append(unchecked: c._slice) + } + } + + // TODO(Windows docs): example with roots, should we rephrase this "spurious + // roots"? + /// Append the contents of `other`, ignoring any spurious leading separators. + /// + /// A leading separator is spurious if `self` is non-empty. + /// + /// Example: + /// + /// var path: FilePath = "" + /// path.append("/var/www/website") // "/var/www/website" + /// path.append("static/assets") // "/var/www/website/static/assets" + /// path.append("/main.css") // "/var/www/website/static/assets/main.css" + public mutating func append(_ other: __owned String) { + defer { _invariantCheck() } + guard !other.utf8.isEmpty else { return } + guard !isEmpty else { + self = FilePath(other) + return + } + let otherPath = FilePath(other) + _append(unchecked: otherPath._storage[otherPath._relativeStart...]) + } + + // TODO(Windows docs): example with roots + /// Non-mutating version of `append(_:Component)`. + public __consuming func appending(_ other: __owned Component) -> FilePath { + var copy = self + copy.append(other) + return copy + } + + // TODO(Windows docs): example with roots + /// Non-mutating version of `append(_:C)`. + public __consuming func appending( + _ components: __owned C + ) -> FilePath where C.Element == FilePath.Component { + var copy = self + copy.append(components) + return copy + } + + // TODO(Windows docs): example with roots + /// Non-mutating version of `append(_:String)`. + public __consuming func appending(_ other: __owned String) -> FilePath { + var copy = self + copy.append(other) + return copy + } + + // TODO(Windows docs): examples and docs with roots, update/generalize doc + // comment + /// If `other` does not have a root, append each component of `other`. If + /// `other` has a root, replaces `self` with other. + /// + /// This operation mimics traversing a directory structure (similar to the + /// `cd` command), where pushing a relative path will append its components + /// and pushing an absolute path will first clear `self`'s existing + /// components. + /// + /// Example: + /// + /// var path: FilePath = "/tmp" + /// path.push("dir/file.txt") // path is "/tmp/dir/file.txt" + /// path.push("/bin") // path is "/bin" + public mutating func push(_ other: __owned FilePath) { + defer { _invariantCheck() } + guard other.root == nil else { + self = other + return + } + // FIXME: Windows drive-relative roots, etc? + _append(unchecked: other._storage[...]) + } + + // TODO(Windows docs): examples and docs with roots + /// Non-mutating version of `push()`. + public __consuming func pushing(_ other: __owned FilePath) -> FilePath { + var copy = self + copy.push(other) + return copy + } + + /// Remove the contents of the path, keeping the null terminator. + public mutating func removeAll(keepingCapacity: Bool = false) { + defer { _invariantCheck() } + _storage.removeAll(keepingCapacity: keepingCapacity) + } + + /// Reserve enough storage space to store `minimumCapacity` platform + /// characters. + public mutating func reserveCapacity(_ minimumCapacity: Int) { + defer { _invariantCheck() } + self._storage.reserveCapacity(minimumCapacity) + } +} + +// MARK - Renamed +@available(System 0.0.2, *) +extension FilePath { + @available(*, unavailable, renamed: "removingLastComponent()") + public var dirname: FilePath { removingLastComponent() } + + @available(*, unavailable, renamed: "lastComponent") + public var basename: Component? { lastComponent } +} diff --git a/Sources/System/FilePath/FilePathTemp.swift b/Sources/System/FilePath/FilePathTemp.swift new file mode 100644 index 00000000..c0edf95d --- /dev/null +++ b/Sources/System/FilePath/FilePathTemp.swift @@ -0,0 +1,97 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 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 +*/ + +// MARK: - API + +/// Create a temporary path for the duration of the closure. +/// +/// - Parameters: +/// - basename: The base name for the temporary path. +/// - body: The closure to execute. +/// +/// Creates a temporary directory with a name based on the given `basename`, +/// executes `body`, passing in the path of the created directory, then +/// deletes the directory and all of its contents before returning. +internal func withTemporaryFilePath( + basename: FilePath.Component, + _ body: (FilePath) throws -> R +) throws -> R { + let temporaryDir = try createUniqueTemporaryDirectory(basename: basename) + defer { + try? _recursiveRemove(at: temporaryDir) + } + + return try body(temporaryDir) +} + +// MARK: - Internals + +fileprivate let base64 = Array( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".utf8 +) + +/// Create a directory that is only accessible to the current user. +/// +/// - Parameters: +/// - path: The path of the directory to create. +/// - Returns: `true` if a new directory was created. +/// +/// This function will throw if there is an error, except if the error +/// is that the directory exists, in which case it returns `false`. +fileprivate func makeLockedDownDirectory(at path: FilePath) throws -> Bool { + return try path.withPlatformString { + if system_mkdir($0, 0o700) == 0 { + return true + } + let err = system_errno + if err == Errno.fileExists.rawValue { + return false + } else { + throw Errno(rawValue: err) + } + } +} + +/// Generate a random string of base64 filename safe characters. +/// +/// - Parameters: +/// - length: The number of characters in the returned string. +/// - Returns: A random string of length `length`. +fileprivate func createRandomString(length: Int) -> String { + return String( + decoding: (0.. FilePath { + var tempDir = try _getTemporaryDirectory() + tempDir.append(basename) + + while true { + tempDir.extension = createRandomString(length: 16) + + if try makeLockedDownDirectory(at: tempDir) { + return tempDir + } + } +} diff --git a/Sources/System/FilePath/FilePathTempPosix.swift b/Sources/System/FilePath/FilePathTempPosix.swift new file mode 100644 index 00000000..e8713c2b --- /dev/null +++ b/Sources/System/FilePath/FilePathTempPosix.swift @@ -0,0 +1,153 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 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) + +/// Get the path to the system temporary directory. +internal func _getTemporaryDirectory() throws -> FilePath { + guard let tmp = system_getenv("TMPDIR") else { + return "/tmp" + } + + return FilePath(SystemString(platformString: tmp)) +} + +/// Delete the entire contents of a directory, including its subdirectories. +/// +/// - Parameters: +/// - path: The directory to be deleted. +/// +/// Removes a directory completely, including all of its contents. +internal func _recursiveRemove( + at path: FilePath +) throws { + let dirfd = try FileDescriptor.open(path, .readOnly, options: .directory) + defer { + try? dirfd.close() + } + + let dot: (CInterop.PlatformChar, CInterop.PlatformChar) = (46, 0) + try withUnsafeBytes(of: dot) { + try recursiveRemove( + in: dirfd.rawValue, + name: $0.assumingMemoryBound(to: CInterop.PlatformChar.self).baseAddress! + ) + } + + try path.withPlatformString { + if system_rmdir($0) != 0 { + throw Errno.current + } + } +} + +/// Open a directory by reference to its parent and name. +/// +/// - Parameters: +/// - dirfd: An open file descriptor for the parent directory. +/// - name: The name of the directory to open. +/// - Returns: A pointer to a `DIR` structure. +/// +/// This is like `opendir()`, but instead of taking a path, it uses a +/// file descriptor pointing at the parent, thus avoiding path length +/// limits. +fileprivate func impl_opendirat( + _ dirfd: CInt, + _ name: UnsafePointer +) -> system_DIRPtr? { + let fd = system_openat(dirfd, name, + FileDescriptor.AccessMode.readOnly.rawValue + | FileDescriptor.OpenOptions.directory.rawValue) + if fd < 0 { + return nil + } + return system_fdopendir(fd) +} + +/// Invoke a closure for each file within a particular directory. +/// +/// - Parameters: +/// - dirfd: The parent of the directory to be enumerated. +/// - subdir: The subdirectory to be enumerated. +/// - body: The closure that will be invoked. +/// +/// We skip the `.` and `..` pseudo-entries. +fileprivate func forEachFile( + in dirfd: CInt, + subdir: UnsafePointer, + _ body: (system_dirent) throws -> () +) throws { + guard let dir = impl_opendirat(dirfd, subdir) else { + throw Errno.current + } + defer { + _ = system_closedir(dir) + } + + while let dirent = system_readdir(dir) { + // Skip . and .. + if dirent.pointee.d_name.0 == 46 + && (dirent.pointee.d_name.1 == 0 + || (dirent.pointee.d_name.1 == 46 + && dirent.pointee.d_name.2 == 0)) { + continue + } + + try body(dirent.pointee) + } +} + +/// Delete the entire contents of a directory, including its subdirectories. +/// +/// - Parameters: +/// - dirfd: The parent of the directory to be removed. +/// - name: The name of the directory to be removed. +/// +/// Removes a directory completely, including all of its contents. +fileprivate func recursiveRemove( + in dirfd: CInt, + name: UnsafePointer +) throws { + // First, deal with subdirectories + try forEachFile(in: dirfd, subdir: name) { dirent in + if dirent.d_type == SYSTEM_DT_DIR { + try withUnsafeBytes(of: dirent.d_name) { + try recursiveRemove( + in: dirfd, + name: $0.assumingMemoryBound(to: CInterop.PlatformChar.self) + .baseAddress! + ) + } + } + } + + // Now delete the contents of this directory + try forEachFile(in: dirfd, subdir: name) { dirent in + let flag: CInt + + if dirent.d_type == SYSTEM_DT_DIR { + flag = SYSTEM_AT_REMOVE_DIR + } else { + flag = 0 + } + + let result = withUnsafeBytes(of: dirent.d_name) { + system_unlinkat(dirfd, + $0.assumingMemoryBound(to: CInterop.PlatformChar.self) + .baseAddress!, + flag) + } + + if result != 0 { + throw Errno.current + } + } +} + +#endif // !os(Windows) diff --git a/Sources/System/FilePath/FilePathTempWindows.swift b/Sources/System/FilePath/FilePathTempWindows.swift new file mode 100644 index 00000000..259aaafe --- /dev/null +++ b/Sources/System/FilePath/FilePathTempWindows.swift @@ -0,0 +1,114 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 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 WinSDK + +/// Get the path to the system temporary directory. +internal func _getTemporaryDirectory() throws -> FilePath { + return try withUnsafeTemporaryAllocation(of: CInterop.PlatformChar.self, + capacity: Int(MAX_PATH) + 1) { + buffer in + + guard GetTempPathW(DWORD(buffer.count), buffer.baseAddress) != 0 else { + throw Errno(windowsError: GetLastError()) + } + + return FilePath(SystemString(platformString: buffer.baseAddress!)) + } +} + +/// Invoke a closure for each file within a particular directory. +/// +/// - Parameters: +/// - path: The path at which we should enumerate items. +/// - body: The closure that will be invoked. +/// +/// We skip the `.` and `..` pseudo-entries. +fileprivate func forEachFile( + at path: FilePath, + _ body: (WIN32_FIND_DATAW) throws -> () +) rethrows { + let searchPath = path.appending("\\*") + + try searchPath.withPlatformString { szPath in + var findData = WIN32_FIND_DATAW() + let hFind = try szPath.withCanonicalPathRepresentation({ szPath in FindFirstFileW(szPath, &findData) }) + if hFind == INVALID_HANDLE_VALUE { + throw Errno(windowsError: GetLastError()) + } + defer { + FindClose(hFind) + } + + repeat { + // Skip . and .. + if findData.cFileName.0 == 46 + && (findData.cFileName.1 == 0 + || (findData.cFileName.1 == 46 + && findData.cFileName.2 == 0)) { + continue + } + + try body(findData) + } while FindNextFileW(hFind, &findData) + } +} + +/// Delete the entire contents of a directory, including its subdirectories. +/// +/// - Parameters: +/// - path: The directory to be deleted. +/// +/// Removes a directory completely, including all of its contents. +internal func _recursiveRemove( + at path: FilePath +) throws { + // First, deal with subdirectories + try forEachFile(at: path) { findData in + if (findData.dwFileAttributes & DWORD(FILE_ATTRIBUTE_DIRECTORY)) != 0 { + let name = withUnsafeBytes(of: findData.cFileName) { + return SystemString(platformString: $0.assumingMemoryBound( + to: CInterop.PlatformChar.self).baseAddress!) + } + let component = FilePath.Component(name)! + let subpath = path.appending(component) + + try _recursiveRemove(at: subpath) + } + } + + // Now delete everything else + try forEachFile(at: path) { findData in + let name = withUnsafeBytes(of: findData.cFileName) { + return SystemString(platformString: $0.assumingMemoryBound( + to: CInterop.PlatformChar.self).baseAddress!) + } + let component = FilePath.Component(name)! + let subpath = path.appending(component) + + if (findData.dwFileAttributes & DWORD(FILE_ATTRIBUTE_DIRECTORY)) == 0 { + try subpath.withPlatformString { subpath in + if try !subpath.withCanonicalPathRepresentation({ DeleteFileW($0) }) { + throw Errno(windowsError: GetLastError()) + } + } + } + } + + // Finally, delete the parent + try path.withPlatformString { + if try !$0.withCanonicalPathRepresentation({ RemoveDirectoryW($0) }) { + throw Errno(windowsError: GetLastError()) + } + } +} + +#endif // os(Windows) diff --git a/Sources/System/FilePath/FilePathWindows.swift b/Sources/System/FilePath/FilePathWindows.swift new file mode 100644 index 00000000..2226816e --- /dev/null +++ b/Sources/System/FilePath/FilePathWindows.swift @@ -0,0 +1,538 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +internal struct _ParsedWindowsRoot { + var rootEnd: SystemString.Index + + // TODO: Remove when I normalize to always (except `C:`) + // have trailing separator + var relativeBegin: SystemString.Index + + var drive: SystemChar? + var fullyQualified: Bool + + var deviceSigil: SystemChar? + + var host: Range? + var volume: Range? +} + +extension _ParsedWindowsRoot { + static func traditional( + drive: SystemChar?, fullQualified: Bool, endingAt idx: SystemString.Index + ) -> _ParsedWindowsRoot { + _ParsedWindowsRoot( + rootEnd: idx, + relativeBegin: idx, + drive: drive, + fullyQualified: fullQualified, + deviceSigil: nil, + host: nil, + volume: nil) + } + + static func unc( + deviceSigil: SystemChar?, + server: Range, + share: Range, + endingAt end: SystemString.Index, + relativeBegin relBegin: SystemString.Index + ) -> _ParsedWindowsRoot { + _ParsedWindowsRoot( + rootEnd: end, + relativeBegin: relBegin, + drive: nil, + fullyQualified: true, + deviceSigil: deviceSigil, + host: server, + volume: share) + } + + static func device( + deviceSigil: SystemChar, + volume: Range, + endingAt end: SystemString.Index, + relativeBegin relBegin: SystemString.Index + ) -> _ParsedWindowsRoot { + _ParsedWindowsRoot( + rootEnd: end, + relativeBegin: relBegin, + drive: nil, + fullyQualified: true, + deviceSigil: deviceSigil, + host: nil, + volume: volume) + } +} + +struct _Lexer { + var slice: Slice + + init(_ str: SystemString) { + self.slice = str[...] + } + + var backslash: SystemChar { .backslash } + + // Try to eat a backslash, returns false if nothing happened + mutating func eatBackslash() -> Bool { + slice._eat(.backslash) != nil + } + + // Try to consume a drive letter and subsequent `:`. + mutating func eatDrive() -> SystemChar? { + let copy = slice + if let d = slice._eat(if: { $0.isLetter }), slice._eat(.colon) != nil { + return d + } + // Restore slice + slice = copy + return nil + } + + // Try to consume a device sigil (stand-alone . or ?) + mutating func eatSigil() -> SystemChar? { + let copy = slice + guard let sigil = slice._eat(.question) ?? slice._eat(.dot) else { + return nil + } + + // Check for something like .hidden or ?question + guard isEmpty || slice.first == backslash else { + slice = copy + return nil + } + + return sigil + } + + // Try to consume an explicit "UNC" directory + mutating func eatUNC() -> Bool { + slice._eatSequence("UNC".unicodeScalars.lazy.map { SystemChar(ascii: $0) }) != nil + } + + // Eat everything up to but not including a backslash or null + mutating func eatComponent() -> Range { + let backslash = self.backslash + let component = slice._eatWhile({ $0 != backslash }) + ?? slice[slice.startIndex ..< slice.startIndex] + return component.indices + } + + var isEmpty: Bool { + return slice.isEmpty + } + + var current: SystemString.Index { slice.startIndex } + + mutating func clear() { + // TODO: Intern empty system string + self = _Lexer(SystemString()) + } + + mutating func reset(to: SystemString, at: SystemString.Index) { + self.slice = to[at...] + } +} + +internal struct WindowsRootInfo { + // The "volume" of a root. For UNC paths, this is also known as the "share". + internal enum Volume: Equatable { + /// No volume specified + /// + /// * Traditional root relative to the current drive: `\`, + /// * Omitted volume from other forms: `\\.\`, `\\.\UNC\server\\`, `\\server\\` + case empty + + // TODO: NT paths? Admin paths using `$`? + /// A specified drive. + /// + /// * Traditional disk: `C:\`, `C:` + /// * Device disk: `\\.\C:\`, `\\?\C:\` + /// * UNC: `\\server\e:\`, `\\?\UNC\server\e:\` + case drive(Character) + + // TODO: GUID type? + /// A volume with a GUID in a non-traditional path + /// + /// * UNC: `\\host\Volume{0000-...}\`, `\\.\UNC\host\Volume{0000-...}\` + /// * Device roots: `\\.\Volume{0000-...}\`, `\\?\Volume{000-...}\` + case guid(String) + + // TODO: Legacy DOS devices, such as COM1? + + /// Device object or share name + /// + /// * Device roots: `\\.\BootPartition\` + /// * UNC: `\\host\volume\` + case volume(String) + + // TODO: Should legacy DOS devices be detected and/or converted at construction time? + // TODO: What about NT paths: `\??\` + } + + /// Represents the syntactic form of the path + internal enum Form: Equatable { + /// Traditional DOS roots: `C:\`, `C:`, and `\` + case traditional(fullyQualified: Bool) // `C:\`, `C:`, `\` + + /// UNC syntactic form: `\\server\share\` + case unc + + /// DOS device syntactic form: `\\?\BootPartition`, `\\.\C:\`, `\\?\UNC\server\share` + case device(sigil: Character) + + // TODO: NT? + } + + /// The host for UNC paths, else `nil`. + internal var host: String? + + /// The specified volume (or UNC share) for the root + internal var volume: Volume + + /// The syntactic form the root is in + internal var form: Form + + init(host: String?, volume: Volume, form: Form) { + self.host = host + self.volume = volume + self.form = form + checkInvariants() + } +} + +extension _ParsedWindowsRoot { + fileprivate func volumeInfo(_ root: SystemString) -> WindowsRootInfo.Volume { + if let d = self.drive { + return .drive(Character(d.asciiScalar!)) + } + + guard let vol = self.volume, !vol.isEmpty else { return .empty } + + // TODO: check for GUID + // TODO: check for drive + return .volume(root[vol].string) + } +} + +extension WindowsRootInfo { + internal init(_ root: SystemString, _ parsed: _ParsedWindowsRoot) { + self.volume = parsed.volumeInfo(root) + + if let host = parsed.host { + self.host = root[host].string + } else { + self.host = nil + } + + if let sig = parsed.deviceSigil { + self.form = .device(sigil: Character(sig.asciiScalar!)) + } else if parsed.host != nil { + assert(parsed.volume != nil) + self.form = .unc + } else { + self.form = .traditional(fullyQualified: parsed.fullyQualified) + } + } +} + +extension WindowsRootInfo { + /// NOT `\foo\bar` nor `C:foo\bar` + internal var isFullyQualified: Bool { + return form != .traditional(fullyQualified: false) + } + + /// + /// `\\server\share\foo\bar.exe`, `\\.\UNC\server\share\foo\bar.exe` + internal var isUNC: Bool { + host != nil + } + + /// + /// `\foo\bar.exe` + internal var isTraditionalRooted: Bool { + form == .traditional(fullyQualified: false) && volume == .empty + } + + /// + /// `C:foo\bar.exe` + internal var isTraditionalDriveRelative: Bool { + switch (form, volume) { + case (.traditional(fullyQualified: false), .drive(_)): return true + default: return false + } + } + + // TODO: Should this be component? + func formPath() -> FilePath { + fatalError("Unimplemented") + } + + // static func traditional( + // drive: Character?, fullyQualified: Bool + // ) -> WindowsRootInfo { + // let vol: Volume + // if let d = Character { + // vol = .drive(d) + // } else { + // vol = .relative + // } + // + // return WindowsRootInfo( + // volume: .relative, form: .traditional(fullyQualified: false)) + // } + + internal func checkInvariants() { + switch form { + case .traditional(let qual): + precondition(host == nil) + switch volume { + case .empty: + precondition(!qual) + break + case .drive(_): break + default: preconditionFailure() + } + case .unc: + precondition(host != nil) + case .device(_): break + } + } + +} + +extension SystemString { + // TODO: Or, should I always inline this to remove some of the bookeeping? + private func _parseWindowsRootInternal() -> _ParsedWindowsRoot? { + assert(_windowsPaths) + + /* + Windows root: device or UNC or DOS + device: (`\\.` or `\\?`) `\` (drive or guid or UNC-link) + drive: letter `:` + guid: `Volume{` (hex-digit or `-`)* `}` + UNC-link: `UNC\` UNC-volume + UNC: `\\` UNC-volume + UNC-volume: server `\` share + DOS: fully-qualified or legacy-device or drive or `\` + full-qualified: drive `\` + + TODO: What is \\?\server1\e:\utilities\\filecomparer\ from the docs? + TODO: What about admin use of `$` instead of `:`? E.g. \\system07\C$\ + + NOTE: Legacy devices are not handled by System at a library level, but + are deferred to the relevant syscalls. + */ + + var lexer = _Lexer(self) + + // Helper to parse a UNC root + func parseUNC(deviceSigil: SystemChar?) -> _ParsedWindowsRoot { + let serverRange = lexer.eatComponent() + guard lexer.eatBackslash() else { + fatalError("expected normalized root to contain backslash") + } + let shareRange = lexer.eatComponent() + let rootEnd = lexer.current + _ = lexer.eatBackslash() + return .unc( + deviceSigil: deviceSigil, + server: serverRange, share: shareRange, + endingAt: rootEnd, relativeBegin: lexer.current) + } + + + // `C:` or `C:\` + if let d = lexer.eatDrive() { + // `C:\` - fully qualified + let fullyQualified = lexer.eatBackslash() + return .traditional( + drive: d, fullQualified: fullyQualified, endingAt: lexer.current) + } + + // `\` or else it's just a rootless relative path + guard lexer.eatBackslash() else { return nil } + + // `\\` or else it's just a current-drive rooted traditional path + guard lexer.eatBackslash() else { + return .traditional( + drive: nil, fullQualified: false, endingAt: lexer.current) + } + + // `\\.` or `\\?` (device paths) or else it's just UNC + guard let sigil = lexer.eatSigil() else { + return parseUNC(deviceSigil: nil) + } + _ = sigil // suppress warnings + + guard lexer.eatBackslash() else { + fatalError("expected normalized root to contain backslash") + } + + if lexer.eatUNC() { + guard lexer.eatBackslash() else { + fatalError("expected normalized root to contain backslash") + } + return parseUNC(deviceSigil: sigil) + } + + let device = lexer.eatComponent() + let rootEnd = lexer.current + _ = lexer.eatBackslash() + + return .device( + deviceSigil: sigil, volume: device, + endingAt: rootEnd, relativeBegin: lexer.current) + } + + @inline(never) + internal func _parseWindowsRoot() -> ( + rootEnd: SystemString.Index, relativeBegin: SystemString.Index + ) { + guard let parsed = _parseWindowsRootInternal() else { + return (startIndex, startIndex) + } + return (parsed.rootEnd, parsed.relativeBegin) + } +} + +extension SystemString { + // UNC and device roots can have multiple repeated roots that are meaningful, + // and extra backslashes may need to be inserted for partial roots (e.g. empty + // volume). + // + // Returns the point where `_normalizeSeparators` should resume. + internal mutating func _prenormalizeWindowsRoots() -> Index { + assert(_windowsPaths) + assert(!self.contains(.slash), "only valid after separator conversion") + + var lexer = _Lexer(self) + + // Only relevant for UNC or device paths + guard lexer.eatBackslash(), lexer.eatBackslash() else { + return lexer.current + } + + // Parse a backslash, inserting one if needed + func expectBackslash() { + if lexer.eatBackslash() { return } + + // A little gross, but we reset the lexer because the lexer + // holds a strong reference to `self`. + // + // TODO: Intern the empty SystemString. Right now, this is + // along an uncommon/pathological case, but we want to in + // general make empty strings without allocation + let idx = lexer.current + lexer.clear() + self.insert(.backslash, at: idx) + lexer.reset(to: self, at: idx) + let p = lexer.eatBackslash() + assert(p) + } + // Parse a component and subsequent backslash, insering one if needed + func expectComponent() { + _ = lexer.eatComponent() + expectBackslash() + } + + // Check for `\\.` style paths + if lexer.eatSigil() != nil { + expectBackslash() + if lexer.eatUNC() { + expectBackslash() + expectComponent() + expectComponent() + return lexer.current + } + expectComponent() + return lexer.current + } + + expectComponent() + expectComponent() + return lexer.current + } +} + +#if os(Windows) +import WinSDK + +// FIXME: Rather than canonicalizing the path at every call site to a Win32 API, +// we should consider always storing absolute paths with the \\?\ prefix applied, +// for better performance. +extension UnsafePointer where Pointee == CInterop.PlatformChar { + /// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee, + /// to ensure long paths greater than MAX_PATH (260) characters are handled correctly. + /// + /// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation + internal func withCanonicalPathRepresentation(_ body: (Self) throws -> Result) throws -> Result { + // 1. Normalize the path first. + // Contrary to the documentation, this works on long paths independently + // of the registry or process setting to enable long paths (but it will also + // not add the \\?\ prefix required by other functions under these conditions). + let dwLength: DWORD = GetFullPathNameW(self, 0, nil, nil) + return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in + guard (1.. DWORD { + DWORD(hr) & 0xffff +} + +@inline(__always) +fileprivate func HRESULT_FACILITY(_ hr: HRESULT) -> DWORD { + DWORD(hr << 16) & 0x1fff +} + +@inline(__always) +fileprivate func SUCCEEDED(_ hr: HRESULT) -> Bool { + hr >= 0 +} + +// This is a non-standard extension to the Windows SDK that allows us to convert +// an HRESULT to a Win32 error code. +@inline(__always) +fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD { + if SUCCEEDED(hr) { return DWORD(ERROR_SUCCESS) } + if HRESULT_FACILITY(hr) == FACILITY_WIN32 { + return HRESULT_CODE(hr) + } + return DWORD(hr) +} +#endif diff --git a/Sources/System/FilePermissions.swift b/Sources/System/FilePermissions.swift index 0b3217b1..f849a997 100644 --- a/Sources/System/FilePermissions.swift +++ b/Sources/System/FilePermissions.swift @@ -17,8 +17,8 @@ /// let perms = FilePermissions(rawValue: 0o644) /// perms == [.ownerReadWrite, .groupRead, .otherRead] // true @frozen -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -public struct FilePermissions: OptionSet, Hashable, Codable { +@available(System 0.0.1, *) +public struct FilePermissions: OptionSet, Sendable, Hashable, Codable { /// The raw C file permissions. @_alwaysEmitIntoClient public let rawValue: CModeT @@ -27,104 +27,101 @@ public struct FilePermissions: OptionSet, Hashable, Codable { @_alwaysEmitIntoClient public init(rawValue: CModeT) { self.rawValue = rawValue } - @_alwaysEmitIntoClient - private init(_ raw: CModeT) { self.init(rawValue: raw) } - /// Indicates that other users have read-only permission. @_alwaysEmitIntoClient - public static var otherRead: FilePermissions { FilePermissions(0o4) } + public static var otherRead: FilePermissions { .init(rawValue: 0o4) } /// Indicates that other users have write-only permission. @_alwaysEmitIntoClient - public static var otherWrite: FilePermissions { FilePermissions(0o2) } + public static var otherWrite: FilePermissions { .init(rawValue: 0o2) } /// Indicates that other users have execute-only permission. @_alwaysEmitIntoClient - public static var otherExecute: FilePermissions { FilePermissions(0o1) } + public static var otherExecute: FilePermissions { .init(rawValue: 0o1) } /// Indicates that other users have read-write permission. @_alwaysEmitIntoClient - public static var otherReadWrite: FilePermissions { FilePermissions(0o6) } + public static var otherReadWrite: FilePermissions { .init(rawValue: 0o6) } /// Indicates that other users have read-execute permission. @_alwaysEmitIntoClient - public static var otherReadExecute: FilePermissions { FilePermissions(0o5) } + public static var otherReadExecute: FilePermissions { .init(rawValue: 0o5) } /// Indicates that other users have write-execute permission. @_alwaysEmitIntoClient - public static var otherWriteExecute: FilePermissions { FilePermissions(0o3) } + public static var otherWriteExecute: FilePermissions { .init(rawValue: 0o3) } /// Indicates that other users have read, write, and execute permission. @_alwaysEmitIntoClient - public static var otherReadWriteExecute: FilePermissions { FilePermissions(0o7) } + public static var otherReadWriteExecute: FilePermissions { .init(rawValue: 0o7) } /// Indicates that the group has read-only permission. @_alwaysEmitIntoClient - public static var groupRead: FilePermissions { FilePermissions(0o40) } + public static var groupRead: FilePermissions { .init(rawValue: 0o40) } /// Indicates that the group has write-only permission. @_alwaysEmitIntoClient - public static var groupWrite: FilePermissions { FilePermissions(0o20) } + public static var groupWrite: FilePermissions { .init(rawValue: 0o20) } /// Indicates that the group has execute-only permission. @_alwaysEmitIntoClient - public static var groupExecute: FilePermissions { FilePermissions(0o10) } + public static var groupExecute: FilePermissions { .init(rawValue: 0o10) } /// Indicates that the group has read-write permission. @_alwaysEmitIntoClient - public static var groupReadWrite: FilePermissions { FilePermissions(0o60) } + public static var groupReadWrite: FilePermissions { .init(rawValue: 0o60) } /// Indicates that the group has read-execute permission. @_alwaysEmitIntoClient - public static var groupReadExecute: FilePermissions { FilePermissions(0o50) } + public static var groupReadExecute: FilePermissions { .init(rawValue: 0o50) } /// Indicates that the group has write-execute permission. @_alwaysEmitIntoClient - public static var groupWriteExecute: FilePermissions { FilePermissions(0o30) } + public static var groupWriteExecute: FilePermissions { .init(rawValue: 0o30) } /// Indicates that the group has read, write, and execute permission. @_alwaysEmitIntoClient - public static var groupReadWriteExecute: FilePermissions { FilePermissions(0o70) } + public static var groupReadWriteExecute: FilePermissions { .init(rawValue: 0o70) } /// Indicates that the owner has read-only permission. @_alwaysEmitIntoClient - public static var ownerRead: FilePermissions { FilePermissions(0o400) } + public static var ownerRead: FilePermissions { .init(rawValue: 0o400) } /// Indicates that the owner has write-only permission. @_alwaysEmitIntoClient - public static var ownerWrite: FilePermissions { FilePermissions(0o200) } + public static var ownerWrite: FilePermissions { .init(rawValue: 0o200) } /// Indicates that the owner has execute-only permission. @_alwaysEmitIntoClient - public static var ownerExecute: FilePermissions { FilePermissions(0o100) } + public static var ownerExecute: FilePermissions { .init(rawValue: 0o100) } /// Indicates that the owner has read-write permission. @_alwaysEmitIntoClient - public static var ownerReadWrite: FilePermissions { FilePermissions(0o600) } + public static var ownerReadWrite: FilePermissions { .init(rawValue: 0o600) } /// Indicates that the owner has read-execute permission. @_alwaysEmitIntoClient - public static var ownerReadExecute: FilePermissions { FilePermissions(0o500) } + public static var ownerReadExecute: FilePermissions { .init(rawValue: 0o500) } /// Indicates that the owner has write-execute permission. @_alwaysEmitIntoClient - public static var ownerWriteExecute: FilePermissions { FilePermissions(0o300) } + public static var ownerWriteExecute: FilePermissions { .init(rawValue: 0o300) } /// Indicates that the owner has read, write, and execute permission. @_alwaysEmitIntoClient - public static var ownerReadWriteExecute: FilePermissions { FilePermissions(0o700) } + public static var ownerReadWriteExecute: FilePermissions { .init(rawValue: 0o700) } /// Indicates that the file is executed as the owner. /// /// For more information, see the `setuid(2)` man page. @_alwaysEmitIntoClient - public static var setUserID: FilePermissions { FilePermissions(0o4000) } + public static var setUserID: FilePermissions { .init(rawValue: 0o4000) } /// Indicates that the file is executed as the group. /// /// For more information, see the `setgid(2)` man page. @_alwaysEmitIntoClient - public static var setGroupID: FilePermissions { FilePermissions(0o2000) } + public static var setGroupID: FilePermissions { .init(rawValue: 0o2000) } /// Indicates that executable's text segment /// should be kept in swap space even after it exits. @@ -132,10 +129,10 @@ public struct FilePermissions: OptionSet, Hashable, Codable { /// For more information, see the `chmod(2)` man page's /// discussion of `S_ISVTX` (the sticky bit). @_alwaysEmitIntoClient - public static var saveText: FilePermissions { FilePermissions(0o1000) } + public static var saveText: FilePermissions { .init(rawValue: 0o1000) } } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) extension FilePermissions : CustomStringConvertible, CustomDebugStringConvertible { diff --git a/Sources/System/FileSystem/FileFlags.swift b/Sources/System/FileSystem/FileFlags.swift new file mode 100644 index 00000000..5a905ed4 --- /dev/null +++ b/Sources/System/FileSystem/FileFlags.swift @@ -0,0 +1,250 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// |------------------------| +// | Swift API to C Mapping | +// |------------------------------------------------------------------| +// | FileFlags | Darwin | FreeBSD | OpenBSD | +// |------------------|---------------|---------------|---------------| +// | noDump | UF_NODUMP | UF_NODUMP | UF_NODUMP | +// | userImmutable | UF_IMMUTABLE | UF_IMMUTABLE | UF_IMMUTABLE | +// | userAppend | UF_APPEND | UF_APPEND | UF_APPEND | +// | archived | SF_ARCHIVED | SF_ARCHIVED | SF_ARCHIVED | +// | systemImmutable | SF_IMMUTABLE | SF_IMMUTABLE | SF_IMMUTABLE | +// | systemAppend | SF_APPEND | SF_APPEND | SF_APPEND | +// | opaque | UF_OPAQUE | UF_OPAQUE | N/A | +// | hidden | UF_HIDDEN | UF_HIDDEN | N/A | +// | systemNoUnlink | SF_NOUNLINK | SF_NOUNLINK | N/A | +// | compressed | UF_COMPRESSED | N/A | N/A | +// | tracked | UF_TRACKED | N/A | N/A | +// | dataVault | UF_DATAVAULT | N/A | N/A | +// | restricted | SF_RESTRICTED | N/A | N/A | +// | firmlink | SF_FIRMLINK | N/A | N/A | +// | dataless | SF_DATALESS | N/A | N/A | +// | userNoUnlink | N/A | UF_NOUNLINK | N/A | +// | offline | N/A | UF_OFFLINE | N/A | +// | readOnly | N/A | UF_READONLY | N/A | +// | reparse | N/A | UF_REPARSE | N/A | +// | sparse | N/A | UF_SPARSE | N/A | +// | system | N/A | UF_SYSTEM | N/A | +// | snapshot | N/A | SF_SNAPSHOT | N/A | +// |------------------|---------------|---------------|---------------| + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + +/// File-specific flags found in the `st_flags` property of a `stat` struct +/// or used as input to `chflags()`. +/// +/// - Note: Only available on Darwin, FreeBSD, and OpenBSD. +@frozen +@available(System 99, *) +public struct FileFlags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + @_alwaysEmitIntoClient + public let rawValue: CInterop.FileFlags + + /// Creates a strongly-typed `FileFlags` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FileFlags) { self.rawValue = rawValue } + + // MARK: Flags Available on Darwin, FreeBSD, and OpenBSD + + /// Do not dump the file during backups. + /// + /// The corresponding C constant is `UF_NODUMP`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var noDump: FileFlags { FileFlags(rawValue: _UF_NODUMP) } + + /// File may not be changed. + /// + /// The corresponding C constant is `UF_IMMUTABLE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var userImmutable: FileFlags { FileFlags(rawValue: _UF_IMMUTABLE) } + + /// Writes to the file may only append. + /// + /// The corresponding C constant is `UF_APPEND`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var userAppend: FileFlags { FileFlags(rawValue: _UF_APPEND) } + + /// File has been archived. + /// + /// The corresponding C constant is `SF_ARCHIVED`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var archived: FileFlags { FileFlags(rawValue: _SF_ARCHIVED) } + + /// File may not be changed. + /// + /// The corresponding C constant is `SF_IMMUTABLE`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var systemImmutable: FileFlags { FileFlags(rawValue: _SF_IMMUTABLE) } + + /// Writes to the file may only append. + /// + /// The corresponding C constant is `SF_APPEND`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var systemAppend: FileFlags { FileFlags(rawValue: _SF_APPEND) } + + // MARK: Flags Available on Darwin and FreeBSD + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Directory is opaque when viewed through a union mount. + /// + /// The corresponding C constant is `UF_OPAQUE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var opaque: FileFlags { FileFlags(rawValue: _UF_OPAQUE) } + + /// File should not be displayed in a GUI. + /// + /// The corresponding C constant is `UF_HIDDEN`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var hidden: FileFlags { FileFlags(rawValue: _UF_HIDDEN) } + + /// File may not be removed or renamed. + /// + /// The corresponding C constant is `SF_NOUNLINK`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var systemNoUnlink: FileFlags { FileFlags(rawValue: _SF_NOUNLINK) } + #endif + + // MARK: Flags Available on Darwin only + + #if SYSTEM_PACKAGE_DARWIN + /// File is compressed at the file system level. + /// + /// The corresponding C constant is `UF_COMPRESSED`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + @_alwaysEmitIntoClient + public static var compressed: FileFlags { FileFlags(rawValue: _UF_COMPRESSED) } + + /// File is tracked for the purpose of document IDs. + /// + /// The corresponding C constant is `UF_TRACKED`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var tracked: FileFlags { FileFlags(rawValue: _UF_TRACKED) } + + /// File requires an entitlement for reading and writing. + /// + /// The corresponding C constant is `UF_DATAVAULT`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var dataVault: FileFlags { FileFlags(rawValue: _UF_DATAVAULT) } + + /// File requires an entitlement for writing. + /// + /// The corresponding C constant is `SF_RESTRICTED`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var restricted: FileFlags { FileFlags(rawValue: _SF_RESTRICTED) } + + /// File is a firmlink. + /// + /// Firmlinks are used by macOS to create transparent links between + /// the read-only system volume and writable data volume. For example, + /// the `/Applications` folder on the system volume is a firmlink to + /// the `/Applications` folder on the data volume, allowing the user + /// to see both system- and user-installed applications in a single folder. + /// + /// The corresponding C constant is `SF_FIRMLINK`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var firmlink: FileFlags { FileFlags(rawValue: _SF_FIRMLINK) } + + /// File is a dataless placeholder (content is stored remotely). + /// + /// The system will attempt to materialize the file when accessed according to + /// the dataless file materialization policy of the accessing thread or process. + /// See `getiopolicy_np(3)`. + /// + /// The corresponding C constant is `SF_DATALESS`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + @_alwaysEmitIntoClient + public static var dataless: FileFlags { FileFlags(rawValue: _SF_DATALESS) } + #endif + + // MARK: Flags Available on FreeBSD Only + + #if os(FreeBSD) + /// File may not be removed or renamed. + /// + /// The corresponding C constant is `UF_NOUNLINK`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var userNoUnlink: FileFlags { FileFlags(rawValue: _UF_NOUNLINK) } + + /// File has the Windows offline attribute. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_OFFLINE` attribute, + /// but otherwise provide no special handling when it's set. + /// + /// The corresponding C constant is `UF_OFFLINE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var offline: FileFlags { FileFlags(rawValue: _UF_OFFLINE) } + + /// File is read-only. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_READONLY` attribute. + /// + /// The corresponding C constant is `UF_READONLY`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var readOnly: FileFlags { FileFlags(rawValue: _UF_READONLY) } + + /// File contains a Windows reparse point. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_REPARSE_POINT` attribute. + /// + /// The corresponding C constant is `UF_REPARSE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var reparse: FileFlags { FileFlags(rawValue: _UF_REPARSE) } + + /// File is sparse. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_SPARSE_FILE` attribute, + /// or to indicate a sparse file. + /// + /// The corresponding C constant is `UF_SPARSE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var sparse: FileFlags { FileFlags(rawValue: _UF_SPARSE) } + + /// File has the Windows system attribute. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_SYSTEM` attribute, + /// but otherwise provide no special handling when it's set. + /// + /// The corresponding C constant is `UF_SYSTEM`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var system: FileFlags { FileFlags(rawValue: _UF_SYSTEM) } + + /// File is a snapshot. + /// + /// The corresponding C constant is `SF_SNAPSHOT`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var snapshot: FileFlags { FileFlags(rawValue: _SF_SNAPSHOT) } + #endif +} +#endif diff --git a/Sources/System/FileSystem/FileMode.swift b/Sources/System/FileSystem/FileMode.swift new file mode 100644 index 00000000..91329f1a --- /dev/null +++ b/Sources/System/FileSystem/FileMode.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) +/// A strongly-typed file mode representing a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +@available(System 99, *) +public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C mode. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed `FileMode` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Mode) { self.rawValue = rawValue } + + /// Creates a `FileMode` from the given file type and permissions. + /// + /// - Note: This initializer masks the inputs with their respective bit masks. + @_alwaysEmitIntoClient + public init(type: FileType, permissions: FilePermissions) { + self.rawValue = (type.rawValue & _MODE_FILETYPE_MASK) | (permissions.rawValue & _MODE_PERMISSIONS_MASK) + } + + /// The file's type, from the mode's file-type bits. + /// + /// Setting this property will mask the `newValue` with the file-type bit mask `S_IFMT`. + @_alwaysEmitIntoClient + public var type: FileType { + get { FileType(rawValue: rawValue & _MODE_FILETYPE_MASK) } + set { rawValue = (rawValue & ~_MODE_FILETYPE_MASK) | (newValue.rawValue & _MODE_FILETYPE_MASK) } + } + + /// The file's permissions, from the mode's permission bits. + /// + /// Setting this property will mask the `newValue` with the permissions bit mask `ALLPERMS`. + @_alwaysEmitIntoClient + public var permissions: FilePermissions { + get { FilePermissions(rawValue: rawValue & _MODE_PERMISSIONS_MASK) } + set { rawValue = (rawValue & ~_MODE_PERMISSIONS_MASK) | (newValue.rawValue & _MODE_PERMISSIONS_MASK) } + } +} +#endif diff --git a/Sources/System/FileSystem/FileType.swift b/Sources/System/FileSystem/FileType.swift new file mode 100644 index 00000000..42134522 --- /dev/null +++ b/Sources/System/FileSystem/FileType.swift @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// |------------------------| +// | Swift API to C Mapping | +// |----------------------------------------| +// | FileType | Unix-like Platforms | +// |------------------|---------------------| +// | directory | S_IFDIR | +// | characterSpecial | S_IFCHR | +// | blockSpecial | S_IFBLK | +// | regular | S_IFREG | +// | fifo | S_IFIFO | +// | symbolicLink | S_IFLNK | +// | socket | S_IFSOCK | +// |------------------|---------------------| +// +// |------------------------------------------------------------------| +// | FileType | Darwin | FreeBSD | Other Unix-like Platforms | +// |------------------|---------|---------|---------------------------| +// | whiteout | S_IFWHT | S_IFWHT | N/A | +// |------------------|---------|---------|---------------------------| + +#if !os(Windows) +/// A file type matching those contained in a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +@available(System 99, *) +public struct FileType: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw file-type bits from the C mode. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed file type from the raw C `mode_t`. + /// + /// - Note: This initializer stores the `rawValue` directly and **does not** + /// mask the value with `S_IFMT`. If the supplied `rawValue` contains bits + /// outside of the `S_IFMT` mask, the resulting `FileType` will not compare + /// equal to constants like `.directory` and `.symbolicLink`, which may + /// be unexpected. + /// + /// If you're unsure whether the `mode_t` contains bits outside of `S_IFMT`, + /// you can use `FileMode(rawValue:)` instead to get a strongly-typed + /// `FileMode`, then call `.type` to get the properly masked `FileType`. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Mode) { self.rawValue = rawValue } + + /// Directory + /// + /// The corresponding C constant is `S_IFDIR`. + @_alwaysEmitIntoClient + public static var directory: FileType { FileType(rawValue: _S_IFDIR) } + + /// Character special device + /// + /// The corresponding C constant is `S_IFCHR`. + @_alwaysEmitIntoClient + public static var characterSpecial: FileType { FileType(rawValue: _S_IFCHR) } + + /// Block special device + /// + /// The corresponding C constant is `S_IFBLK`. + @_alwaysEmitIntoClient + public static var blockSpecial: FileType { FileType(rawValue: _S_IFBLK) } + + /// Regular file + /// + /// The corresponding C constant is `S_IFREG`. + @_alwaysEmitIntoClient + public static var regular: FileType { FileType(rawValue: _S_IFREG) } + + /// FIFO (or named pipe) + /// + /// The corresponding C constant is `S_IFIFO`. + @_alwaysEmitIntoClient + public static var fifo: FileType { FileType(rawValue: _S_IFIFO) } + + /// Symbolic link + /// + /// The corresponding C constant is `S_IFLNK`. + @_alwaysEmitIntoClient + public static var symbolicLink: FileType { FileType(rawValue: _S_IFLNK) } + + /// Socket + /// + /// The corresponding C constant is `S_IFSOCK`. + @_alwaysEmitIntoClient + public static var socket: FileType { FileType(rawValue: _S_IFSOCK) } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Whiteout file + /// + /// The corresponding C constant is `S_IFWHT`. + @_alwaysEmitIntoClient + public static var whiteout: FileType { FileType(rawValue: _S_IFWHT) } + #endif +} +#endif diff --git a/Sources/System/FileSystem/Identifiers.swift b/Sources/System/FileSystem/Identifiers.swift new file mode 100644 index 00000000..b8f90141 --- /dev/null +++ b/Sources/System/FileSystem/Identifiers.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) +/// A Swift wrapper of the C `uid_t` type. +@frozen +@available(System 99, *) +public struct UserID: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `uid_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.UserID + + /// Creates a strongly-typed `UserID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.UserID) { self.rawValue = rawValue } + + /// Creates a strongly-typed `UserID` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.UserID) { self.rawValue = rawValue } +} + +/// A Swift wrapper of the C `gid_t` type. +@frozen +@available(System 99, *) +public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `gid_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.GroupID + + /// Creates a strongly-typed `GroupID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.GroupID) { self.rawValue = rawValue } + + /// Creates a strongly-typed `GroupID` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.GroupID) { self.rawValue = rawValue } +} + +/// A Swift wrapper of the C `dev_t` type. +@frozen +@available(System 99, *) +public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `dev_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.DeviceID + + /// Creates a strongly-typed `DeviceID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.DeviceID) { self.rawValue = rawValue } + + /// Creates a strongly-typed `DeviceID` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.DeviceID) { self.rawValue = rawValue } +} + +/// A Swift wrapper of the C `ino_t` type. +@frozen +@available(System 99, *) +public struct Inode: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `ino_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Inode + + /// Creates a strongly-typed `Inode` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Inode) { self.rawValue = rawValue } + + /// Creates a strongly-typed `Inode` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.Inode) { self.rawValue = rawValue } +} +#endif // !os(Windows) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift new file mode 100644 index 00000000..e30e2b16 --- /dev/null +++ b/Sources/System/FileSystem/Stat.swift @@ -0,0 +1,632 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) + +// Must import here to use C stat properties in @_alwaysEmitIntoClient APIs. +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import CSystem +import Glibc +#elseif canImport(Musl) +import CSystem +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Android) +import CSystem +import Android +#else +#error("Unsupported Platform") +#endif + +// MARK: - Stat + +/// A Swift wrapper of the C `stat` struct. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +@available(System 99, *) +public struct Stat: RawRepresentable, Sendable { + + /// The raw C `stat` struct. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Stat + + /// Creates a Swift `Stat` from the raw C struct. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Stat) { self.rawValue = rawValue } + + // MARK: Stat.Flags + + /// Flags representing those passed to `fstatat()`. + @frozen + public struct Flags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly-typed `Stat.Flags` from raw C flags. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + /// If the path ends with a symbolic link, return information about the link itself. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW`. + @_alwaysEmitIntoClient + public static var symlinkNoFollow: Flags { Flags(rawValue: _AT_SYMLINK_NOFOLLOW) } + + #if SYSTEM_PACKAGE_DARWIN + /// If the path ends with a symbolic link, return information about the link itself. + /// If _any_ symbolic link is encountered during path resolution, return an error. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW_ANY`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var symlinkNoFollowAny: Flags { Flags(rawValue: _AT_SYMLINK_NOFOLLOW_ANY) } + #endif + + #if canImport(Darwin, _version: 346) || os(FreeBSD) + /// If the path does not reside in the hierarchy beneath the starting directory, return an error. + /// + /// The corresponding C constant is `AT_RESOLVE_BENEATH`. + /// - Note: Only available on Darwin and FreeBSD. + @_alwaysEmitIntoClient + public static var resolveBeneath: Flags { Flags(rawValue: _AT_RESOLVE_BENEATH) } + #endif + } + + // MARK: Initializers + + /// Creates a `Stat` struct from a `FilePath`. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._stat( + $0, + followTargetSymlink: followTargetSymlink, + retryOnInterrupt: retryOnInterrupt + ) + }.get() + } + + /// Creates a `Stat` struct from an `UnsafePointer` path. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._stat( + path, + followTargetSymlink: followTargetSymlink, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _stat( + _ ptr: UnsafePointer, + followTargetSymlink: Bool, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + if followTargetSymlink { + system_stat(ptr, &result) + } else { + system_lstat(ptr, &result) + } + }.map { result } + } + + /// Creates a `Stat` struct from a `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + @_alwaysEmitIntoClient + public init( + _ fd: FileDescriptor, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstat( + fd, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _fstat( + _ fd: FileDescriptor, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstat(fd.rawValue, &result) + }.map { result } + } + + /// Creates a `Stat` struct from a `FilePath` and `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._fstatat( + $0, + relativeTo: _AT_FDCWD, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ) + }.get() + } + + /// Creates a `Stat` struct from a `FilePath` and `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._fstatat( + $0, + relativeTo: fd.rawValue, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ) + }.get() + } + + /// Creates a `Stat` struct from an `UnsafePointer` path and `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstatat( + path, + relativeTo: _AT_FDCWD, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + /// Creates a `Stat` struct from an `UnsafePointer` path and `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstatat( + path, + relativeTo: fd.rawValue, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _fstatat( + _ path: UnsafePointer, + relativeTo fd: FileDescriptor.RawValue, + flags: Stat.Flags, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstatat(fd, path, &result, flags.rawValue) + }.map { result } + } + + + // MARK: Properties + + /// ID of device containing file + /// + /// The corresponding C property is `st_dev`. + @_alwaysEmitIntoClient + public var deviceID: DeviceID { + get { DeviceID(rawValue: numericCast(rawValue.st_dev)) } + set { rawValue.st_dev = numericCast(newValue.rawValue) } + } + + /// Inode number + /// + /// The corresponding C property is `st_ino`. + @_alwaysEmitIntoClient + public var inode: Inode { + get { Inode(rawValue: numericCast(rawValue.st_ino)) } + set { rawValue.st_ino = numericCast(newValue.rawValue) } + } + + /// File mode + /// + /// The corresponding C property is `st_mode`. + @_alwaysEmitIntoClient + public var mode: FileMode { + get { FileMode(rawValue: numericCast(rawValue.st_mode)) } + set { rawValue.st_mode = numericCast(newValue.rawValue) } + } + + /// File type for the given mode + /// + /// - Note: This property is equivalent to `mode.type`. Modifying this + /// property will update the underlying `st_mode` accordingly. + @_alwaysEmitIntoClient + public var type: FileType { + get { mode.type } + set { + var newMode = mode + newMode.type = newValue + mode = newMode + } + } + + /// File permissions for the given mode + /// + /// - Note: This property is equivalent to `mode.permissions`. Modifying + /// this property will update the underlying `st_mode` accordingly. + @_alwaysEmitIntoClient + public var permissions: FilePermissions { + get { mode.permissions } + set { + var newMode = mode + newMode.permissions = newValue + mode = newMode + } + } + + /// Number of hard links + /// + /// The corresponding C property is `st_nlink`. + @_alwaysEmitIntoClient + public var linkCount: Int { + get { Int(rawValue.st_nlink) } + set { rawValue.st_nlink = numericCast(newValue) } + } + + /// User ID of owner + /// + /// The corresponding C property is `st_uid`. + @_alwaysEmitIntoClient + public var userID: UserID { + get { UserID(rawValue: rawValue.st_uid) } + set { rawValue.st_uid = newValue.rawValue } + } + + /// Group ID of owner + /// + /// The corresponding C property is `st_gid`. + @_alwaysEmitIntoClient + public var groupID: GroupID { + get { GroupID(rawValue: rawValue.st_gid) } + set { rawValue.st_gid = newValue.rawValue } + } + + /// Device ID (if special file) + /// + /// For character or block special files, the returned `DeviceID` may have + /// meaningful major and minor values. For non-special files, this + /// property is usually meaningless and often set to 0. + /// + /// The corresponding C property is `st_rdev`. + @_alwaysEmitIntoClient + public var specialDeviceID: DeviceID { + get { DeviceID(rawValue: numericCast(rawValue.st_rdev)) } + set { rawValue.st_rdev = numericCast(newValue.rawValue) } + } + + /// Total size, in bytes + /// + /// The semantics of this property are tied to the underlying C `st_size` field, + /// which can have file-system–dependent behavior. For example, this property + /// can return different values for a file's data fork and resource fork, and some + /// file systems report logical size rather than actual disk usage for compressed + /// or cloned files. + /// + /// The corresponding C property is `st_size`. + @_alwaysEmitIntoClient + public var size: Int64 { + get { Int64(rawValue.st_size) } + set { rawValue.st_size = numericCast(newValue) } + } + + /// Block size for file system I/O, in bytes + /// + /// The corresponding C property is `st_blksize`. + @_alwaysEmitIntoClient + public var preferredIOBlockSize: Int { + get { Int(rawValue.st_blksize) } + set { rawValue.st_blksize = numericCast(newValue) } + } + + /// Number of 512-byte blocks allocated + /// + /// The semantics of this property are tied to the underlying C `st_blocks` field, + /// which can have file-system–dependent behavior. + /// + /// The corresponding C property is `st_blocks`. + @_alwaysEmitIntoClient + public var blocksAllocated: Int64 { + get { Int64(rawValue.st_blocks) } + set { rawValue.st_blocks = numericCast(newValue) } + } + + /// Total size allocated, in bytes + /// + /// The semantics of this property are tied to the underlying C `st_blocks` field, + /// which can have file-system–dependent behavior. + /// + /// - Note: Calculated as `512 * blocksAllocated`. + @_alwaysEmitIntoClient + public var sizeAllocated: Int64 { + 512 * blocksAllocated + } + + // NOTE: "st_" property names are used for the `timespec` properties so + // we can reserve `accessTime`, `modificationTime`, etc. for potential + // `UTCClock.Instant` properties in the future. + + /// Time of last access, given as a C `timespec` since the Epoch. + /// + /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). + @_alwaysEmitIntoClient + public var st_atim: timespec { + get { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_atimespec + #else + rawValue.st_atim + #endif + } + set { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_atimespec = newValue + #else + rawValue.st_atim = newValue + #endif + } + } + + /// Time of last modification, given as a C `timespec` since the Epoch. + /// + /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). + @_alwaysEmitIntoClient + public var st_mtim: timespec { + get { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_mtimespec + #else + rawValue.st_mtim + #endif + } + set { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_mtimespec = newValue + #else + rawValue.st_mtim = newValue + #endif + } + } + + /// Time of last status (inode) change, given as a C `timespec` since the Epoch. + /// + /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). + @_alwaysEmitIntoClient + public var st_ctim: timespec { + get { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_ctimespec + #else + rawValue.st_ctim + #endif + } + set { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_ctimespec = newValue + #else + rawValue.st_ctim = newValue + #endif + } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Time of file creation, given as a C `timespec` since the Epoch. + /// + /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). + /// - Note: Only available on Darwin and FreeBSD. + @_alwaysEmitIntoClient + public var st_birthtim: timespec { + get { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_birthtimespec + #else + rawValue.st_birthtim + #endif + } + set { + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_birthtimespec = newValue + #else + rawValue.st_birthtim = newValue + #endif + } + } + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + /// File flags + /// + /// The corresponding C property is `st_flags`. + /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. + @_alwaysEmitIntoClient + public var flags: FileFlags { + get { FileFlags(rawValue: rawValue.st_flags) } + set { rawValue.st_flags = newValue.rawValue } + } + + /// File generation number + /// + /// The file generation number is used to distinguish between different files + /// that have used the same inode over time. + /// + /// The corresponding C property is `st_gen`. + /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. + @_alwaysEmitIntoClient + public var generationNumber: Int { + get { Int(rawValue.st_gen) } + set { rawValue.st_gen = numericCast(newValue)} + } + #endif +} + +// MARK: - Equatable and Hashable + +@available(System 99, *) +extension Stat: Equatable { + @_alwaysEmitIntoClient + /// Compares the raw bytes of two `Stat` structs for equality. + public static func == (lhs: Self, rhs: Self) -> Bool { + return withUnsafeBytes(of: lhs.rawValue) { lhsBytes in + withUnsafeBytes(of: rhs.rawValue) { rhsBytes in + lhsBytes.elementsEqual(rhsBytes) + } + } + } +} + +@available(System 99, *) +extension Stat: Hashable { + @_alwaysEmitIntoClient + /// Hashes the raw bytes of this `Stat` struct. + public func hash(into hasher: inout Hasher) { + withUnsafeBytes(of: rawValue) { bytes in + hasher.combine(bytes: bytes) + } + } +} + +// MARK: - CustomStringConvertible and CustomDebugStringConvertible + +// MARK: - FileDescriptor Extensions + +@available(System 99, *) +extension FileDescriptor { + + /// Creates a `Stat` struct for the file referenced by this `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + @_alwaysEmitIntoClient + public func stat( + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try Stat(self, retryOnInterrupt: retryOnInterrupt) + } +} + +// MARK: - FilePath Extensions + +@available(System 99, *) +extension FilePath { + + /// Creates a `Stat` struct for the file referenced by this `FilePath`. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + @_alwaysEmitIntoClient + public func stat( + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try Stat(self, followTargetSymlink: followTargetSymlink, retryOnInterrupt: retryOnInterrupt) + } + + /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public func stat( + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try Stat(self, flags: flags, retryOnInterrupt: retryOnInterrupt) + } + + /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public func stat( + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try Stat(self, relativeTo: fd, flags: flags, retryOnInterrupt: retryOnInterrupt) + } +} + +#endif // !os(Windows) diff --git a/Sources/System/IORing/IOCompletion.swift b/Sources/System/IORing/IOCompletion.swift new file mode 100644 index 00000000..d9e69050 --- /dev/null +++ b/Sources/System/IORing/IOCompletion.swift @@ -0,0 +1,71 @@ +#if compiler(>=6.2) && $Lifetimes +#if os(Linux) + +import CSystem + +public extension IORing { + struct Completion: ~Copyable { + @inlinable init(rawValue inRawValue: io_uring_cqe) { + rawValue = inRawValue + } + @usableFromInline let rawValue: io_uring_cqe + } +} + +public extension IORing.Completion { + struct Flags: OptionSet, Hashable, Codable { + public let rawValue: UInt32 + + @inlinable public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + ///`IORING_CQE_F_BUFFER` Indicates the buffer ID is stored in the upper 16 bits + @inlinable public static var allocatedBuffer: Flags { Flags(rawValue: 1 << 0) } + ///`IORING_CQE_F_MORE` Indicates more completions will be generated from the request that generated this + @inlinable public static var moreCompletions: Flags { Flags(rawValue: 1 << 1) } + //`IORING_CQE_F_SOCK_NONEMPTY`, but currently unused + //@inlinable public static var socketNotEmpty: Flags { Flags(rawValue: 1 << 2) } + //`IORING_CQE_F_NOTIF`, but currently unused + //@inlinable public static var isNotificationEvent: Flags { Flags(rawValue: 1 << 3) } + //IORING_CQE_F_BUF_MORE will eventually be (1U << 4) if we add IOU_PBUF_RING_INC support + } +} + +public extension IORing.Completion { + @inlinable var context: UInt64 { + get { + rawValue.user_data + } + } + + @inlinable var userPointer: UnsafeRawPointer? { + get { + UnsafeRawPointer(bitPattern: UInt(rawValue.user_data)) + } + } + + @inlinable var result: Int32 { + get { + rawValue.res + } + } + + @inlinable var flags: IORing.Completion.Flags { + get { + Flags(rawValue: rawValue.flags & 0x0000FFFF) + } + } + + @inlinable var bufferIndex: UInt16? { + get { + if self.flags.contains(.allocatedBuffer) { + return UInt16(rawValue.flags >> 16) + } else { + return nil + } + } + } +} +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift new file mode 100644 index 00000000..17be7927 --- /dev/null +++ b/Sources/System/IORing/IORequest.swift @@ -0,0 +1,497 @@ +#if compiler(>=6.2) && $Lifetimes +#if os(Linux) + +import CSystem + +@usableFromInline +internal enum IORequestCore { + case nop // nothing here + case openat( + atDirectory: FileDescriptor, + path: FilePath, + FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + context: UInt64 = 0 + ) + case openatSlot( + atDirectory: FileDescriptor, + path: FilePath, + FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + intoSlot: IORing.RegisteredFile, + context: UInt64 = 0 + ) + case read( + file: FileDescriptor, + buffer: IORing.RegisteredBuffer, + offset: UInt64 = 0, + context: UInt64 = 0 + ) + case readUnregistered( + file: FileDescriptor, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64 = 0, + context: UInt64 = 0 + ) + case readSlot( + file: IORing.RegisteredFile, + buffer: IORing.RegisteredBuffer, + offset: UInt64 = 0, + context: UInt64 = 0 + ) + case readUnregisteredSlot( + file: IORing.RegisteredFile, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64 = 0, + context: UInt64 = 0 + ) + case write( + file: FileDescriptor, + buffer: IORing.RegisteredBuffer, + offset: UInt64 = 0, + context: UInt64 = 0 + ) + case writeUnregistered( + file: FileDescriptor, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64 = 0, + context: UInt64 = 0 + ) + case writeSlot( + file: IORing.RegisteredFile, + buffer: IORing.RegisteredBuffer, + offset: UInt64 = 0, + context: UInt64 = 0 + ) + case writeUnregisteredSlot( + file: IORing.RegisteredFile, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64 = 0, + context: UInt64 = 0 + ) + case close( + FileDescriptor, + context: UInt64 = 0 + ) + case closeSlot( + IORing.RegisteredFile, + context: UInt64 = 0 + ) + case unlinkAt( + atDirectory: FileDescriptor, + path: FilePath, + context: UInt64 = 0 + ) + case cancel( + flags:UInt32 + ) + case cancelContext( + flags: UInt32, + targetContext: UInt64 + ) + case cancelFD( + flags: UInt32, + targetFD: FileDescriptor + ) + case cancelFDSlot( + flags: UInt32, + target: IORing.RegisteredFile + ) + +} + +@inline(__always) @inlinable +internal func makeRawRequest_readWrite_registered( + file: FileDescriptor, + buffer: IORing.RegisteredBuffer, + offset: UInt64, + context: UInt64 = 0, + request: consuming RawIORequest +) -> RawIORequest { + request.fileDescriptor = file + request.buffer = buffer.unsafeBuffer + request.rawValue.buf_index = UInt16(exactly: buffer.index)! + request.offset = offset + request.rawValue.user_data = context + return request +} + +@inline(__always) @inlinable +internal func makeRawRequest_readWrite_registered_slot( + file: IORing.RegisteredFile, + buffer: IORing.RegisteredBuffer, + offset: UInt64, + context: UInt64 = 0, + request: consuming RawIORequest +) -> RawIORequest { + request.rawValue.fd = Int32(exactly: file.index)! + request.flags = .fixedFile + request.buffer = buffer.unsafeBuffer + request.rawValue.buf_index = UInt16(exactly: buffer.index)! + request.offset = offset + request.rawValue.user_data = context + return request +} + +@inline(__always) @inlinable +internal func makeRawRequest_readWrite_unregistered( + file: FileDescriptor, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64, + context: UInt64 = 0, + request: consuming RawIORequest +) -> RawIORequest { + request.fileDescriptor = file + request.buffer = buffer + request.offset = offset + request.rawValue.user_data = context + return request +} + +@inline(__always) @inlinable +internal func makeRawRequest_readWrite_unregistered_slot( + file: IORing.RegisteredFile, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64, + context: UInt64 = 0, + request: consuming RawIORequest +) -> RawIORequest { + request.rawValue.fd = Int32(exactly: file.index)! + request.flags = .fixedFile + request.buffer = buffer + request.offset = offset + request.rawValue.user_data = context + return request +} + +extension IORing { + public struct Request { + @usableFromInline var core: IORequestCore + + @inlinable internal init(core inCore: IORequestCore) { + core = inCore + } + + @inlinable internal consuming func extractCore() -> IORequestCore { + return core + } + } +} + + + +extension IORing.Request { + @inlinable public static func nop(context: UInt64 = 0) -> IORing.Request { + .init(core: .nop) + } + + @inlinable public static func read( + _ file: IORing.RegisteredFile, + into buffer: IORing.RegisteredBuffer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .readSlot(file: file, buffer: buffer, offset: offset, context: context)) + } + + @inlinable public static func read( + _ file: FileDescriptor, + into buffer: IORing.RegisteredBuffer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .read(file: file, buffer: buffer, offset: offset, context: context)) + } + + @inlinable public static func read( + _ file: IORing.RegisteredFile, + into buffer: UnsafeMutableRawBufferPointer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .readUnregisteredSlot(file: file, buffer: buffer, offset: offset, context: context)) + } + + @inlinable public static func read( + _ file: FileDescriptor, + into buffer: UnsafeMutableRawBufferPointer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .readUnregistered(file: file, buffer: buffer, offset: offset, context: context)) + } + + @inlinable public static func write( + _ buffer: IORing.RegisteredBuffer, + into file: IORing.RegisteredFile, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .writeSlot(file: file, buffer: buffer, offset: offset, context: context)) + } + + @inlinable public static func write( + _ buffer: IORing.RegisteredBuffer, + into file: FileDescriptor, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .write(file: file, buffer: buffer, offset: offset, context: context)) + } + + @inlinable public static func write( + _ buffer: UnsafeMutableRawBufferPointer, + into file: IORing.RegisteredFile, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .writeUnregisteredSlot( + file: file, buffer: buffer, offset: offset, context: context)) + } + + @inlinable public static func write( + _ buffer: UnsafeMutableRawBufferPointer, + into file: FileDescriptor, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORing.Request { + .init( + core: .writeUnregistered(file: file, buffer: buffer, offset: offset, context: context) + ) + } + + @inlinable public static func close( + _ file: FileDescriptor, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .close(file, context: context)) + } + + @inlinable public static func close( + _ file: IORing.RegisteredFile, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .closeSlot(file, context: context)) + } + + @inlinable public static func open( + _ path: FilePath, + in directory: FileDescriptor, + into slot: IORing.RegisteredFile, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + context: UInt64 = 0 + ) -> IORing.Request { + .init( + core: .openatSlot( + atDirectory: directory, path: path, mode, options: options, + permissions: permissions, intoSlot: slot, context: context)) + } + + @inlinable public static func open( + _ path: FilePath, + in directory: FileDescriptor, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + context: UInt64 = 0 + ) -> IORing.Request { + .init( + core: .openat( + atDirectory: directory, path: path, mode, options: options, + permissions: permissions, context: context + )) + } + + @inlinable public static func unlink( + _ path: FilePath, + in directory: FileDescriptor, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .unlinkAt(atDirectory: directory, path: path, context: context)) + } + + // Cancel + + /* + * ASYNC_CANCEL flags. + * + * IORING_ASYNC_CANCEL_ALL Cancel all requests that match the given key + * IORING_ASYNC_CANCEL_FD Key off 'fd' for cancellation rather than the + * request 'user_data' + * IORING_ASYNC_CANCEL_ANY Match any request + * IORING_ASYNC_CANCEL_FD_FIXED 'fd' passed in is a fixed descriptor + * IORING_ASYNC_CANCEL_USERDATA Match on user_data, default for no other key + * IORING_ASYNC_CANCEL_OP Match request based on opcode + */ + +@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_ALL: UInt32 { 1 << 0 } +@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_FD: UInt32 { 1 << 1 } +@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_ANY: UInt32 { 1 << 2 } +@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_FD_FIXED: UInt32 { 1 << 3 } +@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_USERDATA: UInt32 { 1 << 4 } +@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_OP: UInt32 { 1 << 5 } + + public enum CancellationMatch { + case all + case first + } + + @inlinable public static func cancel( + _ matchAll: CancellationMatch, + matchingContext: UInt64 + ) -> IORing.Request { + switch matchAll { + case .all: + .init(core: .cancelContext(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + case .first: + .init(core: .cancelContext(flags: SWIFT_IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + } + } + + @inlinable public static func cancel( + _ matchAll: CancellationMatch, + matching: FileDescriptor + ) -> IORing.Request { + switch matchAll { + case .all: + .init(core: .cancelFD(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_FD, targetFD: matching)) + case .first: + .init(core: .cancelFD(flags: SWIFT_IORING_ASYNC_CANCEL_FD, targetFD: matching)) + } + } + + @inlinable public static func cancel( + _ matchAll: CancellationMatch, + matching: IORing.RegisteredFile + ) -> IORing.Request { + switch matchAll { + case .all: + .init(core: .cancelFDSlot(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) + case .first: + .init(core: .cancelFDSlot(flags: SWIFT_IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) + } + } + + @inlinable public static func cancel( + _ matchAll: CancellationMatch, + ) -> IORing.Request { + switch matchAll { + case .all: + .init(core: .cancel(flags: SWIFT_IORING_ASYNC_CANCEL_ALL)) + case .first: + .init(core: .cancel(flags: SWIFT_IORING_ASYNC_CANCEL_ANY)) + } + } + + @inline(__always) @inlinable + internal consuming func makeRawRequest() -> RawIORequest { + var request = RawIORequest() + switch extractCore() { + case .nop: + request.operation = .nop + case .openatSlot( + let atDirectory, let path, let mode, let options, let permissions, let fileSlot, + let context): + // TODO: use rawValue less + request.operation = .openAt + request.fileDescriptor = atDirectory + request.rawValue.addr = UInt64( + UInt( + bitPattern: path.withPlatformString { ptr in + ptr //this is unsavory, but we keep it alive by storing path alongside it in the request + })) + request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) + request.rawValue.len = permissions?.rawValue ?? 0 + request.rawValue.file_index = UInt32(fileSlot.index + 1) + request.path = path + request.rawValue.user_data = context + case .openat( + let atDirectory, let path, let mode, let options, let permissions, let context): + request.operation = .openAt + request.fileDescriptor = atDirectory + request.rawValue.addr = UInt64( + UInt( + bitPattern: path.withPlatformString { ptr in + ptr //this is unsavory, but we keep it alive by storing path alongside it in the request + })) + request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) + request.rawValue.len = permissions?.rawValue ?? 0 + request.path = path + request.rawValue.user_data = context + case .write(let file, let buffer, let offset, let context): + request.operation = .writeFixed + return makeRawRequest_readWrite_registered( + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .writeSlot(let file, let buffer, let offset, let context): + request.operation = .writeFixed + return makeRawRequest_readWrite_registered_slot( + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .writeUnregistered(let file, let buffer, let offset, let context): + request.operation = .write + return makeRawRequest_readWrite_unregistered( + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .writeUnregisteredSlot(let file, let buffer, let offset, let context): + request.operation = .write + return makeRawRequest_readWrite_unregistered_slot( + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .read(let file, let buffer, let offset, let context): + request.operation = .readFixed + return makeRawRequest_readWrite_registered( + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .readSlot(let file, let buffer, let offset, let context): + request.operation = .readFixed + return makeRawRequest_readWrite_registered_slot( + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .readUnregistered(let file, let buffer, let offset, let context): + request.operation = .read + return makeRawRequest_readWrite_unregistered( + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .readUnregisteredSlot(let file, let buffer, let offset, let context): + request.operation = .read + return makeRawRequest_readWrite_unregistered_slot( + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .close(let file, let context): + request.operation = .close + request.fileDescriptor = file + request.rawValue.user_data = context + case .closeSlot(let file, let context): + request.operation = .close + request.rawValue.file_index = UInt32(file.index + 1) + request.rawValue.user_data = context + case .unlinkAt(let atDirectory, let path, let context): + request.operation = .unlinkAt + request.fileDescriptor = atDirectory + request.rawValue.addr = UInt64( + UInt( + bitPattern: path.withPlatformString { ptr in + ptr //this is unsavory, but we keep it alive by storing path alongside it in the request + }) + ) + request.path = path + request.rawValue.user_data = context + case .cancelContext(let flags, let targetContext): + request.operation = .asyncCancel + request.cancel_flags = flags + request.addr = targetContext + case .cancelFD(let flags, let targetFD): + request.operation = .asyncCancel + request.cancel_flags = flags + request.fileDescriptor = targetFD + case .cancelFDSlot(let flags, let target): + request.operation = .asyncCancel + request.cancel_flags = flags + request.rawValue.fd = Int32(target.index) + case .cancel(let flags): + request.operation = .asyncCancel + request.cancel_flags = flags + } + + return request + } +} +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift new file mode 100644 index 00000000..16c85a0f --- /dev/null +++ b/Sources/System/IORing/IORing.swift @@ -0,0 +1,904 @@ +#if compiler(>=6.2) && $Lifetimes +#if os(Linux) + +import CSystem +// needed for mmap +#if canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#endif +import Synchronization + +private var ioringSupported: Bool { + __SWIFT_IORING_SUPPORTED != 0 +} + +//This was #defines in older headers, so we redeclare it to get a consistent import +internal enum RegistrationOps: UInt32 { + case registerBuffers = 0 + case unregisterBuffers = 1 + case registerFiles = 2 + case unregisterFiles = 3 + case registerEventFD = 4 + case unregisterEventFD = 5 + case registerFilesUpdate = 6 + case registerEventFDAsync = 7 + case registerProbe = 8 + case registerPersonality = 9 + case unregisterPersonality = 10 +} + +extension UnsafeMutableRawPointer { + func advanced(by offset: UInt32) -> UnsafeMutableRawPointer { + return advanced(by: Int(offset)) + } +} + +extension UnsafeMutableRawBufferPointer { + func to_iovec() -> iovec { + iovec(iov_base: baseAddress, iov_len: count) + } +} + +// all pointers in this struct reference kernel-visible memory +@usableFromInline struct SQRing: ~Copyable { + @usableFromInline let kernelHead: UnsafePointer> + @usableFromInline let kernelTail: UnsafePointer> + @usableFromInline var userTail: UInt32 + + // from liburing: the kernel should never change these + // might change in the future with resizable rings? + @usableFromInline let ringMask: UInt32 + // let ringEntries: UInt32 - absorbed into array.count + + // ring flags bitfield + // currently used by the kernel only in SQPOLL mode to indicate + // when the polling thread needs to be woken up + @usableFromInline let flags: UnsafePointer> + + // ring array + // maps indexes between the actual ring and the submissionQueueEntries list, + // allowing the latter to be used as a kind of freelist with enough work? + // currently, just 1:1 mapping (0.. +} + +@usableFromInline struct CQRing: ~Copyable { + @usableFromInline let kernelHead: UnsafePointer> + @usableFromInline let kernelTail: UnsafePointer> + + @usableFromInline let ringMask: UInt32 + + @usableFromInline let cqes: UnsafeBufferPointer +} + +@inline(__always) @inlinable +internal func _tryWriteRequest( + _ request: __owned RawIORequest, ring: inout SQRing, + submissionQueueEntries: UnsafeMutableBufferPointer +) + -> Bool +{ + if let entry = _getSubmissionEntry( + ring: &ring, submissionQueueEntries: submissionQueueEntries) { + entry.pointee = request.rawValue + return true + } + return false +} + +//TODO: omitting signal mask for now +//Tell the kernel that we've submitted requests and/or are waiting for completions +@inlinable +internal func _enter( + ring: borrowing SQRing, + ringDescriptor: Int32, + numEvents: UInt32, + minCompletions: UInt32, + flags: UInt32 +) throws(Errno) -> Int32 { + // Ring always needs enter right now; + // TODO: support SQPOLL here + while true { + let ret = io_uring_enter(ringDescriptor, numEvents, minCompletions, flags, nil) + // error handling: + // EAGAIN / EINTR (try again), + // EBADF / EBADFD / EOPNOTSUPP / ENXIO + // (failure in ring lifetime management, fatal), + // EINVAL (bad constant flag?, fatal), + // EFAULT (bad address for argument from library, fatal) + if ret == -EAGAIN || ret == -EINTR { + //TODO: should we wait a bit on AGAIN? + continue + } else if ret < 0 { + throw(Errno(rawValue: -ret)) + } else if _getSubmissionQueueCount(ring: ring) > 0 { + // See https://github.com/axboe/liburing/issues/309, in some cases not all pending requests are submitted + continue + } else { + return ret + } + } +} + +@inlinable +internal func _submitRequests(ring: borrowing SQRing, ringDescriptor: Int32) throws(Errno) { + let flushedEvents = _flushQueue(ring: ring) + _ = try _enter( + ring: ring, ringDescriptor: ringDescriptor, numEvents: flushedEvents, minCompletions: 0, flags: 0) +} + +@inlinable +internal func _getSubmissionQueueCount(ring: borrowing SQRing) -> UInt32 { + return ring.userTail - ring.kernelHead.pointee.load(ordering: .acquiring) +} + +@inlinable +internal func _getRemainingSubmissionQueueCapacity(ring: borrowing SQRing) -> UInt32 { + return UInt32(truncatingIfNeeded: ring.array.count) - _getSubmissionQueueCount(ring: ring) +} + +@inlinable +internal func _getUnconsumedCompletionCount(ring: borrowing CQRing) -> UInt32 { + return ring.kernelTail.pointee.load(ordering: .acquiring) + - ring.kernelHead.pointee.load(ordering: .acquiring) +} + +@inlinable +internal func _flushQueue(ring: borrowing SQRing) -> UInt32 { + ring.kernelTail.pointee.store( + ring.userTail, ordering: .releasing + ) + return _getSubmissionQueueCount(ring: ring) +} + +@inlinable +internal func _getSubmissionEntry( + ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer +) -> UnsafeMutablePointer< + swift_io_uring_sqe +>? { + let next = ring.userTail &+ 1 //this is expected to wrap + + let kernelHead: UInt32 = ring.kernelHead.pointee.load(ordering: .acquiring) + + // FEAT: 128-bit event support (not in MVP) + if next - kernelHead <= ring.array.count { + // let sqe = &sq->sqes[(sq->sqe_tail & sq->ring_mask) << shift]; + let sqeIndex = Int( + ring.userTail & ring.ringMask + ) + + let sqe = submissionQueueEntries + .baseAddress.unsafelyUnwrapped + .advanced(by: sqeIndex) + + ring.userTail = next + return sqe + } + return nil +} + +private func setUpRing( + queueDepth: UInt32, flags: IORing.SetupFlags +) throws(Errno) -> + (params: io_uring_params, ringDescriptor: Int32, ringPtr: UnsafeMutableRawPointer?, ringSize: Int, submissionRingPtr: UnsafeMutableRawPointer?, submissionRingSize: Int, completionRingPtr: UnsafeMutableRawPointer?, completionRingSize: Int, sqes: UnsafeMutableRawPointer) { + var params = io_uring_params() + params.flags = flags.rawValue + + var err: Errno? = nil + let ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { + let result = io_uring_setup(queueDepth, $0) + if result < 0 { + err = Errno.current + } + return result + } + + if let err { + throw err + } + + if params.features & IORing.Features.nonDroppingCompletions.rawValue == 0 + { + close(ringDescriptor) + throw Errno.invalidArgument + } + + let submitRingSize = + params.sq_off.array + + params.sq_entries * UInt32(MemoryLayout.size) + + let completionRingSize = + params.cq_off.cqes + + params.cq_entries * UInt32(MemoryLayout.size) + + let ringSize = Int(max(submitRingSize, completionRingSize)) + + var ringPtr: UnsafeMutableRawPointer! + var sqPtr: UnsafeMutableRawPointer! + var cqPtr: UnsafeMutableRawPointer! + + if params.features & IORING_FEAT_SINGLE_MMAP != 0{ + ringPtr = mmap( + /* addr: */ nil, + /* len: */ ringSize, + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ off_t(IORING_OFF_SQ_RING) + ) + + if ringPtr == MAP_FAILED { + let errno = Errno.current + close(ringDescriptor) + throw errno + } + } else { + sqPtr = mmap( + /* addr: */ nil, + /* len: */ Int(submitRingSize), + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ off_t(IORING_OFF_SQ_RING) + ) + + if sqPtr == MAP_FAILED { + let errno = Errno.current + close(ringDescriptor) + throw errno + } + + cqPtr = mmap( + /* addr: */ nil, + /* len: */ Int(completionRingSize), + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ off_t(IORING_OFF_CQ_RING) + ) + + if cqPtr == MAP_FAILED { + let errno: Errno = Errno.current + close(ringDescriptor) + throw errno + } + } + + // map the submission queue + let sqes = mmap( + /* addr: */ nil, + /* len: */ Int(params.sq_entries) * MemoryLayout.size, + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ off_t(IORING_OFF_SQES) + ) + + if sqes == MAP_FAILED { + let errno = Errno.current + if ringPtr != nil { + munmap(ringPtr, ringSize) + } else { + if sqPtr != nil { + munmap(sqPtr, Int(submitRingSize)) + } + if cqPtr != nil { + munmap(cqPtr, Int(completionRingSize)) + } + } + close(ringDescriptor) + throw errno + } + + return (params: params, ringDescriptor: ringDescriptor, ringPtr: ringPtr, ringSize: ringSize, submissionRingPtr: sqPtr, submissionRingSize: Int(submitRingSize), completionRingPtr: cqPtr, completionRingSize: Int(completionRingSize), sqes: sqes!) +} + +///IORing provides facilities for +/// * Registering and unregistering resources (files and buffers), an `io_uring` specific variation on Unix file IOdescriptors that improves their efficiency +/// * Registering and unregistering eventfds, which allow asynchronous waiting for completions +/// * Enqueueing IO requests +/// * Dequeueing IO completions +public struct IORing: ~Copyable { + let ringFlags: UInt32 + @usableFromInline let ringDescriptor: Int32 + + @usableFromInline var submissionRing: SQRing + // FEAT: set this eventually + let submissionPolling: Bool = false + + @usableFromInline let completionRing: CQRing + + @usableFromInline let submissionQueueEntries: UnsafeMutableBufferPointer + + // kept around for unmap / cleanup. TODO: we can save a few words of memory by figuring out how to handle cleanup for non-IORING_FEAT_SINGLE_MMAP better + let ringSize: Int + let ringPtr: UnsafeMutableRawPointer? + let submissionRingSize: Int + let submissionRingPtr: UnsafeMutableRawPointer? + let completionRingSize: Int + let completionRingPtr: UnsafeMutableRawPointer? + + @usableFromInline var _registeredFiles: [UInt32] + @usableFromInline var _registeredBuffers: [iovec] + + var features = Features(rawValue: 0) + + /// RegisteredResource is used via its typealiases, RegisteredFile and RegisteredBuffer. Registering file descriptors and buffers with the IORing allows for more efficient access to them. + public struct RegisteredResource { + public typealias Resource = T + @usableFromInline let resource: T + public let index: Int + + @inlinable internal init( + resource: T, + index: Int + ) { + self.resource = resource + self.index = index + } + } + + public typealias RegisteredFile = RegisteredResource + public typealias RegisteredBuffer = RegisteredResource + + /// SetupFlags represents configuration options to an IORing as it's being created + public struct SetupFlags: OptionSet, RawRepresentable, Hashable { + public var rawValue: UInt32 + + @inlinable public init(rawValue: UInt32) { + self.rawValue = rawValue + } + @inlinable public static var pollCompletions: SetupFlags { .init(rawValue: UInt32(1) << 0) } //IORING_SETUP_IOPOLL + @inlinable public static var pollSubmissions: SetupFlags { .init(rawValue: UInt32(1) << 1) } //IORING_SETUP_SQPOLL + //TODO: figure out how to expose IORING_SETUP_SQ_AFF, IORING_SETUP_CQSIZE, IORING_SETUP_ATTACH_WQ + @inlinable public static var clampMaxEntries: SetupFlags { .init(rawValue: UInt32(1) << 4) } //IORING_SETUP_CLAMP + @inlinable public static var startDisabled: SetupFlags { .init(rawValue: UInt32(1) << 6) } //IORING_SETUP_R_DISABLED + @inlinable public static var continueSubmittingOnError: SetupFlags { .init(rawValue: UInt32(1) << 7) } //IORING_SETUP_SUBMIT_ALL + //TODO: do we want to expose IORING_SETUP_COOP_TASKRUN and IORING_SETUP_TASKRUN_FLAG? + //public static var runTasksCooperatively: SetupFlags { .init(rawValue: UInt32(1) << 8) } //IORING_SETUP_COOP_TASKRUN + //TODO: can we even do different size sqe/cqe? It requires a kernel feature, but how do we convince swift to let the types be different sizes? + //internal static var use128ByteSQEs: SetupFlags { .init(rawValue: UInt32(1) << 10) } //IORING_SETUP_SQE128 + //internal static var use32ByteCQEs: SetupFlags { .init(rawValue: UInt32(1) << 11) } //IORING_SETUP_CQE32 + @inlinable public static var singleSubmissionThread: SetupFlags { .init(rawValue: UInt32(1) << 12) } //IORING_SETUP_SINGLE_ISSUER + @inlinable public static var deferRunningTasks: SetupFlags { .init(rawValue: UInt32(1) << 13) } //IORING_SETUP_DEFER_TASKRUN + //pretty sure we don't want to expose IORING_SETUP_NO_MMAP or IORING_SETUP_REGISTERED_FD_ONLY currently + //TODO: should IORING_SETUP_NO_SQARRAY be the default? do we need to adapt anything to it? + } + + /// Initializes an IORing with enough space for `queueDepth` prepared requests and completed operations + public init(queueDepth: UInt32, flags: SetupFlags = []) throws(Errno) { + guard ioringSupported else { + throw Errno.notSupported + } + + let (params, tmpRingDescriptor, tmpRingPtr, tmpRingSize, tmpSQPtr, tmpSQSize, tmpCQPtr, tmpCQSize, sqes) = try setUpRing(queueDepth: queueDepth, flags: flags) + // All throws need to be before initializing ivars here to avoid + // "error: conditional initialization or destruction of noncopyable types is not supported; + // this variable must be consistently in an initialized or uninitialized state through every code path" + + // Pre-compute values to avoid accessing partially initialized state + let ringBasePtr = tmpRingPtr ?? tmpSQPtr! + let completionBasePtr = tmpRingPtr ?? tmpCQPtr! + + let submissionRing = SQRing( + kernelHead: UnsafePointer>( + ringBasePtr.advanced(by: params.sq_off.head) + .assumingMemoryBound(to: Atomic.self) + ), + kernelTail: UnsafePointer>( + ringBasePtr.advanced(by: params.sq_off.tail) + .assumingMemoryBound(to: Atomic.self) + ), + userTail: 0, // no requests yet + ringMask: ringBasePtr.advanced(by: params.sq_off.ring_mask) + .assumingMemoryBound(to: UInt32.self).pointee, + flags: UnsafePointer>( + ringBasePtr.advanced(by: params.sq_off.flags) + .assumingMemoryBound(to: Atomic.self) + ), + array: UnsafeMutableBufferPointer( + start: ringBasePtr.advanced(by: params.sq_off.array) + .assumingMemoryBound(to: UInt32.self), + count: Int( + ringBasePtr.advanced(by: params.sq_off.ring_entries) + .assumingMemoryBound(to: UInt32.self).pointee) + ) + ) + + let completionRing = CQRing( + kernelHead: UnsafePointer>( + completionBasePtr.advanced(by: params.cq_off.head) + .assumingMemoryBound(to: Atomic.self) + ), + kernelTail: UnsafePointer>( + completionBasePtr.advanced(by: params.cq_off.tail) + .assumingMemoryBound(to: Atomic.self) + ), + ringMask: completionBasePtr.advanced(by: params.cq_off.ring_mask) + .assumingMemoryBound(to: UInt32.self).pointee, + cqes: UnsafeBufferPointer( + start: completionBasePtr.advanced(by: params.cq_off.cqes) + .assumingMemoryBound(to: io_uring_cqe.self), + count: Int( + completionBasePtr.advanced(by: params.cq_off.ring_entries) + .assumingMemoryBound(to: UInt32.self).pointee) + ) + ) + + let submissionQueueEntries = UnsafeMutableBufferPointer( + start: sqes.assumingMemoryBound(to: swift_io_uring_sqe.self), + count: Int(params.sq_entries) + ) + + // Now initialize all stored properties + self.features = Features(rawValue: params.features) + self.ringDescriptor = tmpRingDescriptor + self.ringPtr = tmpRingPtr + self.ringSize = tmpRingSize + self.submissionRingPtr = tmpSQPtr + self.submissionRingSize = tmpSQSize + self.completionRingPtr = tmpCQPtr + self.completionRingSize = tmpCQSize + self._registeredFiles = [] + self._registeredBuffers = [] + self.submissionRing = submissionRing + self.completionRing = completionRing + self.submissionQueueEntries = submissionQueueEntries + self.ringFlags = params.flags + + // fill submission ring array with 1:1 map to underlying SQEs + // (happens after all properties are initialized) + for i in 0 ..< self.submissionRing.array.count { + self.submissionRing.array[i] = UInt32(i) + } + } + + @inlinable + internal func _blockingConsumeCompletionGuts( + minimumCount: UInt32, + maximumCount: UInt32, + extraArgs: UnsafeMutablePointer? = nil, + consumer: (consuming IORing.Completion?, Errno?, Bool) throws(Err) -> Void + ) throws(Err) { + var count = 0 + while let completion = _tryConsumeCompletion(ring: completionRing) { + count += 1 + if completion.result < 0 { + try consumer(nil, Errno(rawValue: -completion.result), false) + } else { + try consumer(completion, nil, false) + } + if count == maximumCount { + try consumer(nil, nil, true) + return + } + } + + if count < minimumCount { + while count < minimumCount { + var sz = 0 + if extraArgs != nil { + sz = MemoryLayout.size + } + let res = io_uring_enter2( + ringDescriptor, + 0, + minimumCount, + IORING_ENTER_GETEVENTS, + extraArgs, + sz + ) + // error handling: + // EAGAIN / EINTR (try again), + // EBADF / EBADFD / EOPNOTSUPP / ENXIO + // (failure in ring lifetime management, fatal), + // EINVAL (bad constant flag?, fatal), + // EFAULT (bad address for argument from library, fatal) + // EBUSY (not enough space for events; implies events filled + // by kernel between kernelTail load and now) + if res >= 0 || res == -EBUSY { + break + } else if res == -EAGAIN || res == -EINTR { + continue + } + fatalError( + "fatal error in receiving requests: " + + Errno(rawValue: -res).debugDescription + ) + } + var count = 0 + while let completion = _tryConsumeCompletion(ring: completionRing) { + count += 1 + if completion.result < 0 { + try consumer(nil, Errno(rawValue: -completion.result), false) + } else { + try consumer(completion, nil, false) + } + if count == maximumCount { + break + } + } + try consumer(nil, nil, true) + } + } + + @inlinable + internal func _blockingConsumeOneCompletion( + extraArgs: UnsafeMutablePointer? = nil + ) throws(Errno) -> Completion { + var result: Completion? = nil + try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { + (completion: consuming Completion?, error, done) throws(Errno) in + if let error { + throw error + } + if let completion { + result = consume completion + } + } + return result.take()! + } + + /// Synchronously waits for an operation to complete for up to `timeout` (or forever if not specified) + @inlinable + public func blockingConsumeCompletion( + timeout: Duration? = nil + ) throws(Errno) -> Completion { + if let timeout { + var ts = timespec( + tv_sec: Int(timeout.components.seconds), + tv_nsec: Int(timeout.components.attoseconds / 1_000_000_000) + ) + return try withUnsafePointer(to: &ts) { (tsPtr) throws(Errno) -> Completion in + var args = swift_io_uring_getevents_arg( + sigmask: 0, + sigmask_sz: 0, + min_wait_usec: 0, + ts: UInt64(UInt(bitPattern: tsPtr)) + ) + return try _blockingConsumeOneCompletion(extraArgs: &args) + } + } else { + return try _blockingConsumeOneCompletion() + } + } + + /// Synchronously waits for `minimumCount` or more operations to complete for up to `timeout` (or forever if not specified). For each completed operation found, `consumer` is called to handle processing it + @inlinable + public func blockingConsumeCompletions( + minimumCount: UInt32 = 1, + timeout: Duration? = nil, + consumer: (consuming Completion?, Errno?, Bool) throws(Err) -> Void + ) throws(Err) { + if let timeout { + var ts = timespec( + tv_sec: Int(timeout.components.seconds), + tv_nsec: Int(timeout.components.attoseconds / 1_000_000_000) + ) + try withUnsafePointer(to: &ts) { (tsPtr) throws(Err) in + var args = swift_io_uring_getevents_arg( + sigmask: 0, + sigmask_sz: 0, + min_wait_usec: 0, + ts: UInt64(UInt(bitPattern: tsPtr)) + ) + try _blockingConsumeCompletionGuts( + minimumCount: minimumCount, maximumCount: UInt32.max, extraArgs: &args, + consumer: consumer) + } + } else { + try _blockingConsumeCompletionGuts( + minimumCount: minimumCount, maximumCount: UInt32.max, consumer: consumer) + } + } + + // public func peekNextCompletion() -> IOCompletion { + + // } + + /// Takes a completed operation from the ring and returns it, if one is ready. Otherwise, returns nil. + @inlinable + public func tryConsumeCompletion() -> Completion? { + return _tryConsumeCompletion(ring: completionRing) + } + + @inlinable + func _tryConsumeCompletion(ring: borrowing CQRing) -> Completion? { + let tail = ring.kernelTail.pointee.load(ordering: .acquiring) + let head = ring.kernelHead.pointee.load(ordering: .acquiring) + + if tail != head { + // 32 byte copy - oh well + let res = ring.cqes[Int(head & ring.ringMask)] + ring.kernelHead.pointee.store(head &+ 1, ordering: .releasing) + return Completion(rawValue: res) + } + + return nil + } + + /// Registers an event monitoring file descriptor with the ring. The file descriptor becomes readable whenever completions are ready to be dequeued. See `man eventfd(2)` for additional information. + public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(Errno) { + var rawfd = descriptor.rawValue + let result = withUnsafePointer(to: &rawfd) { fdptr in + let result = io_uring_register( + ringDescriptor, + RegistrationOps.registerEventFD.rawValue, + UnsafeMutableRawPointer(mutating: fdptr), + 1 + ) + return result >= 0 ? nil : Errno(rawValue: -result) + } + if let result { + throw result + } + } + + /// Removes a registered event file descriptor from the ring + public mutating func unregisterEventFD() throws(Errno) { + let result = io_uring_register( + ringDescriptor, + RegistrationOps.unregisterEventFD.rawValue, + nil, + 0 + ) + if result < 0 { + throw Errno(rawValue: -result) + } + } + + /// Registers `count` files with the ring for later use in IO operations + public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources { + precondition(_registeredFiles.isEmpty) + precondition(count < UInt32.max) + let files = [UInt32](repeating: UInt32.max, count: count) + + let regResult = files.withUnsafeBufferPointer { bPtr in + let result = io_uring_register( + self.ringDescriptor, + RegistrationOps.registerFiles.rawValue, + UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), + UInt32(truncatingIfNeeded: count) + ) + return result >= 0 ? nil : Errno(rawValue: -result) + } + + if let regResult { + throw regResult + } + + _registeredFiles = files + return registeredFileSlots + } + + /// Removes registered files from the ring + public func unregisterFiles() throws { + let result = io_uring_register( + ringDescriptor, + RegistrationOps.unregisterFiles.rawValue, + nil, + 0 + ) + if result < 0 { + throw Errno(rawValue: -result) + } + } + + /// Allows access to registered files by index + @inlinable + public var registeredFileSlots: RegisteredResources { + RegisteredResources(resources: _registeredFiles) + } + + /// Registers buffers with the ring for later use in IO operations + public mutating func registerBuffers(_ buffers: some Collection) throws(Errno) + -> RegisteredResources + { + precondition(buffers.count < UInt32.max) + precondition(_registeredBuffers.isEmpty) + let iovecs = buffers.map { $0.to_iovec() } + let regResult = iovecs.withUnsafeBufferPointer { bPtr in + let result = io_uring_register( + self.ringDescriptor, + RegistrationOps.registerBuffers.rawValue, + UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), + UInt32(truncatingIfNeeded: buffers.count) + ) + return result >= 0 ? nil : Errno(rawValue: -result) + } + + if let regResult { + throw regResult + } + + _registeredBuffers = iovecs + return registeredBuffers + } + + /// Registers buffers with the ring for later use in IO operations + @inlinable + public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) throws(Errno) + -> RegisteredResources + { + try registerBuffers(buffers) + } + + /// A view of the registered files or buffers in a ring + public struct RegisteredResources: RandomAccessCollection { + @usableFromInline let resources: [T] + + @inlinable public var startIndex: Int { 0 } + @inlinable public var endIndex: Int { resources.endIndex } + @inlinable init(resources: [T]) { + self.resources = resources + } + @inlinable public subscript(position: Int) -> RegisteredResource { + RegisteredResource(resource: resources[position], index: position) + } + @inlinable public subscript(position: UInt16) -> RegisteredResource { + RegisteredResource(resource: resources[Int(position)], index: Int(position)) + } + } + + /// Allows access to registered files by index + @inlinable + public var registeredBuffers: RegisteredResources { + RegisteredResources(resources: _registeredBuffers) + } + + public func unregisterBuffers() throws { + let result = io_uring_register( + self.ringDescriptor, + RegistrationOps.unregisterBuffers.rawValue, + nil, + 0 + ) + guard result >= 0 else { + throw Errno(rawValue: -result) + } + } + + /// Sends all prepared requests to the kernel for processing. Results will be delivered as completions, which can be dequeued from the ring. + @inlinable + public func submitPreparedRequests() throws(Errno) { + try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) + } + + /// Sends all prepared requests to the kernel for processing, and then dequeues at least `minimumCount` completions, waiting up to `timeout` for them to become available. `consumer` is called to process each completed IO operation as it becomes available. + @inlinable + public func submitPreparedRequestsAndConsumeCompletions( + minimumCount: UInt32 = 1, + timeout: Duration? = nil, + consumer: (consuming Completion?, Errno?, Bool) throws(Err) -> Void + ) throws(Err) { + //TODO: optimize this to one uring_enter + do { + try submitPreparedRequests() + } catch (let e) { + try consumer(nil, e, true) + } + try blockingConsumeCompletions( + minimumCount: minimumCount, + timeout: timeout, + consumer: consumer + ) + } + + /// Attempts to prepare an IO request for submission to the kernel. Returns false if no space is available to enqueue the request + @inlinable + public mutating func prepare(request: __owned Request) -> Bool { + var raw: RawIORequest? = request.makeRawRequest() + return _tryWriteRequest( + raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) + } + + /// Attempts to prepare a chain of linked IO requests for submission to the kernel. Returns false if not enough space is available to enqueue the request. If any linked operation fails, subsequent operations will be canceled. Linked operations always execute in order. + @inlinable + mutating func prepare(linkedRequests: some BidirectionalCollection) -> Bool { + guard linkedRequests.count > 0 else { + return true + } + let freeSQECount = _getRemainingSubmissionQueueCapacity(ring: submissionRing) + guard freeSQECount >= linkedRequests.count else { + return false + } + let last = linkedRequests.last! + for req in linkedRequests.dropLast() { + var raw = req.makeRawRequest() + raw.linkToNextRequest() + let successfullyAdded = _tryWriteRequest( + raw, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) + assert(successfullyAdded) + } + let successfullyAdded = _tryWriteRequest( + last.makeRawRequest(), ring: &submissionRing, + submissionQueueEntries: submissionQueueEntries) + assert(successfullyAdded) + return true + } + + /// Prepares a sequence of requests for submission to the ring. Returns false if the submission queue doesn't have enough available space. + @inlinable + public mutating func prepare(linkedRequests: Request...) -> Bool { + prepare(linkedRequests: linkedRequests) + } + + /// Prepares and submits a sequence of requests to the ring. Returns false if the submission queue doesn't have enough available space. + @inlinable + public mutating func submit(linkedRequests: Request...) throws(Errno) -> Bool { + if !prepare(linkedRequests: linkedRequests) { + return false + } + try submitPreparedRequests() + return true + } + + /// Describes which io_uring features are supported by the kernel this program is running on + public struct Features: OptionSet, RawRepresentable, Hashable { + public let rawValue: UInt32 + + @inlinable public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + //IORING_FEAT_SINGLE_MMAP is handled internally + @inlinable public static var nonDroppingCompletions: Features { .init(rawValue: UInt32(1) << 1) } //IORING_FEAT_NODROP + @inlinable public static var stableSubmissions: Features { .init(rawValue: UInt32(1) << 2) } //IORING_FEAT_SUBMIT_STABLE + @inlinable public static var currentFilePosition: Features { .init(rawValue: UInt32(1) << 3) } //IORING_FEAT_RW_CUR_POS + @inlinable public static var assumingTaskCredentials: Features { .init(rawValue: UInt32(1) << 4) } //IORING_FEAT_CUR_PERSONALITY + @inlinable public static var fastPolling: Features { .init(rawValue: UInt32(1) << 5) } //IORING_FEAT_FAST_POLL + @inlinable public static var epoll32BitFlags: Features { .init(rawValue: UInt32(1) << 6) } //IORING_FEAT_POLL_32BITS + @inlinable public static var pollNonFixedFiles: Features { .init(rawValue: UInt32(1) << 7) } //IORING_FEAT_SQPOLL_NONFIXED + @inlinable public static var extendedArguments: Features { .init(rawValue: UInt32(1) << 8) } //IORING_FEAT_EXT_ARG + @inlinable public static var nativeWorkers: Features { .init(rawValue: UInt32(1) << 9) } //IORING_FEAT_NATIVE_WORKERS + @inlinable public static var resourceTags: Features { .init(rawValue: UInt32(1) << 10) } //IORING_FEAT_RSRC_TAGS + @inlinable public static var allowsSkippingSuccessfulCompletions: Features { .init(rawValue: UInt32(1) << 11) } //IORING_FEAT_CQE_SKIP + @inlinable public static var improvedLinkedFiles: Features { .init(rawValue: UInt32(1) << 12) } //IORING_FEAT_LINKED_FILE + @inlinable public static var registerRegisteredRings: Features { .init(rawValue: UInt32(1) << 13) } //IORING_FEAT_REG_REG_RING + @inlinable public static var minimumTimeout: Features { .init(rawValue: UInt32(1) << 15) } //IORING_FEAT_MIN_TIMEOUT + @inlinable public static var bundledSendReceive: Features { .init(rawValue: UInt32(1) << 14) } //IORING_FEAT_RECVSEND_BUNDLE + } + + /// Describes which io_uring features are supported by the kernel this program is running on + public var supportedFeatures: Features { + return features + } + + deinit { + if let ringPtr { + munmap(ringPtr, ringSize) + } else if let submissionRingPtr, let completionRingPtr { + munmap(submissionRingPtr, submissionRingSize) + munmap(completionRingPtr, completionRingSize) + } + munmap( + UnsafeMutableRawPointer(submissionQueueEntries.baseAddress!), + submissionQueueEntries.count * MemoryLayout.size + ) + close(ringDescriptor) + } +} + +extension IORing.RegisteredBuffer { + @unsafe @inlinable public var unsafeBuffer: UnsafeMutableRawBufferPointer { + return .init(start: resource.iov_base, count: resource.iov_len) + } + + @inlinable public var mutableBytes: MutableRawSpan { + @_lifetime(&self) + mutating get { + let span = MutableRawSpan(_unsafeBytes: unsafeBuffer) + return unsafe _overrideLifetime(span, mutating: &self) + } + } + + @inlinable public var bytes: RawSpan { + let span = RawSpan(_unsafeBytes: UnsafeRawBufferPointer(unsafeBuffer)) + return unsafe _overrideLifetime(span, borrowing: self) + } +} +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Sources/System/IORing/RawIORequest.swift b/Sources/System/IORing/RawIORequest.swift new file mode 100644 index 00000000..0bc40294 --- /dev/null +++ b/Sources/System/IORing/RawIORequest.swift @@ -0,0 +1,205 @@ +#if compiler(>=6.2) && $Lifetimes +#if os(Linux) + +import CSystem + +@usableFromInline +internal struct RawIORequest: ~Copyable { + // swift_io_uring_sqe is a typedef of io_uring_sqe on platforms where + // IORing is supported (currently requires kernel version >= 5.15). + @usableFromInline var rawValue: swift_io_uring_sqe + @usableFromInline var path: FilePath? //buffer owner for the path pointer that the sqe may have + + @inlinable public init() { + self.rawValue = swift_io_uring_sqe() + } +} + +extension RawIORequest { + @usableFromInline + enum Operation: UInt8 { + case nop = 0 + case readv = 1 + case writev = 2 + case fsync = 3 + case readFixed = 4 + case writeFixed = 5 + case pollAdd = 6 + case pollRemove = 7 + case syncFileRange = 8 + case sendMessage = 9 + case receiveMessage = 10 + // ... + case asyncCancel = 14 + case link_timeout = 15 + // ... + case openAt = 18 + case close = 19 + case filesUpdate = 20 + case statx = 21 + case read = 22 + case write = 23 + // ... + case openAt2 = 28 + // ... + case unlinkAt = 36 + } + + public struct Flags: OptionSet, Hashable, Codable { + public let rawValue: UInt8 + + @inlinable public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + @inlinable public static var fixedFile: RawIORequest.Flags { Flags(rawValue: 1 << 0) } + @inlinable public static var drainQueue: RawIORequest.Flags { Flags(rawValue: 1 << 1) } + @inlinable public static var linkRequest: RawIORequest.Flags { Flags(rawValue: 1 << 2) } + @inlinable public static var hardlinkRequest: RawIORequest.Flags { Flags(rawValue: 1 << 3) } + @inlinable public static var asynchronous: RawIORequest.Flags { Flags(rawValue: 1 << 4) } + @inlinable public static var selectBuffer: RawIORequest.Flags { Flags(rawValue: 1 << 5) } + @inlinable public static var skipSuccess: RawIORequest.Flags { Flags(rawValue: 1 << 6) } + } + + @inlinable var operation: Operation { + get { Operation(rawValue: rawValue.opcode)! } + set { rawValue.opcode = newValue.rawValue } + } + + @inlinable var cancel_flags: UInt32 { + get { rawValue.cancel_flags } + set { rawValue.cancel_flags = newValue } + } + + @inlinable var addr: UInt64 { + get { rawValue.addr } + set { rawValue.addr = newValue } + } + + @inlinable public var flags: Flags { + get { Flags(rawValue: rawValue.flags) } + set { rawValue.flags = newValue.rawValue } + } + + @inlinable public mutating func linkToNextRequest() { + flags = Flags(rawValue: flags.rawValue | Flags.linkRequest.rawValue) + } + + @inlinable public var fileDescriptor: FileDescriptor { + get { FileDescriptor(rawValue: rawValue.fd) } + set { rawValue.fd = newValue.rawValue } + } + + @inlinable public var offset: UInt64? { + get { + if (rawValue.off == UInt64.max) { + return nil + } else { + return rawValue.off + } + } + set { + if let val = newValue { + rawValue.off = val + } else { + rawValue.off = UInt64.max + } + } + } + + @inlinable public var buffer: UnsafeMutableRawBufferPointer { + get { + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(exactly: rawValue.addr)!) + return UnsafeMutableRawBufferPointer(start: ptr, count: Int(rawValue.len)) + } + + set { + rawValue.addr = UInt64(Int(bitPattern: newValue.baseAddress!)) + rawValue.len = UInt32(exactly: newValue.count)! + } + } + + public enum RequestFlags { + case readWriteFlags(ReadWriteFlags) + // case fsyncFlags(FsyncFlags?) + // poll_events + // poll32_events + // sync_range_flags + // msg_flags + case timeoutFlags(TimeOutFlags) + // accept_flags + // cancel_flags + case openFlags(FileDescriptor.OpenOptions) + // statx_flags + // fadvise_advice + // splice_flags + } + + public struct ReadWriteFlags: OptionSet, Hashable, Codable { + public var rawValue: UInt32 + @inlinable public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + @inlinable public static var highPriority: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 0) } + + // sync with only data integrity + @inlinable public static var dataSync: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 1) } + + // sync with full data + file integrity + @inlinable public static var fileSync: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 2) } + + // return -EAGAIN if operation blocks + @inlinable public static var noWait: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 3) } + + // append to end of the file + @inlinable public static var append: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 4) } + } + + public struct TimeOutFlags: OptionSet, Hashable, Codable { + public var rawValue: UInt32 + + @inlinable public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + @inlinable public static var relativeTime: RawIORequest.TimeOutFlags { TimeOutFlags(rawValue: 0) } + @inlinable public static var absoluteTime: RawIORequest.TimeOutFlags { TimeOutFlags(rawValue: 1 << 0) } + } +} + +extension RawIORequest { + @inlinable + static func nop() -> RawIORequest { + var req: RawIORequest = RawIORequest() + req.operation = .nop + return req + } + + @inlinable + static func withTimeoutRequest( + linkedTo opEntry: UnsafeMutablePointer, + in timeoutEntry: UnsafeMutablePointer, + duration: Duration, + flags: TimeOutFlags, + work: () throws -> R) rethrows -> R { + + opEntry.pointee.flags |= Flags.linkRequest.rawValue + opEntry.pointee.off = 1 + var ts = timespec( + tv_sec: Int(duration.components.seconds), + tv_nsec: Int(duration.components.attoseconds / 1_000_000_000) + ) + return try withUnsafePointer(to: &ts) { tsPtr in + var req: RawIORequest = RawIORequest() + req.operation = .link_timeout + req.rawValue.timeout_flags = flags.rawValue + req.rawValue.len = 1 + req.rawValue.addr = UInt64(UInt(bitPattern: tsPtr)) + timeoutEntry.pointee = req.rawValue + return try work() + } + } +} +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Sources/System/Internals/Backcompat.swift b/Sources/System/Internals/Backcompat.swift new file mode 100644 index 00000000..a376dd2e --- /dev/null +++ b/Sources/System/Internals/Backcompat.swift @@ -0,0 +1,29 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 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 +*/ + +extension String { + internal init( + _unsafeUninitializedCapacity capacity: Int, + initializingUTF8With body: (UnsafeMutableBufferPointer) throws -> Int + ) rethrows { + if #available(macOS 11, iOS 14.0, watchOS 7.0, tvOS 14.0, *) { + self = try String( + unsafeUninitializedCapacity: capacity, + initializingUTF8With: body) + return + } + + let array = try Array( + unsafeUninitializedCapacity: capacity + ) { buffer, count in + count = try body(buffer) + } + self = String(decoding: array, as: UTF8.self) + } +} diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift new file mode 100644 index 00000000..7f85b9e7 --- /dev/null +++ b/Sources/System/Internals/CInterop.swift @@ -0,0 +1,95 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2024 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 SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif os(Windows) +import CSystem +import ucrt +#elseif canImport(Glibc) +@_implementationOnly import CSystem +import Glibc +#elseif canImport(Musl) +@_implementationOnly import CSystem +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Bionic) +import CSystem +import Bionic +#else +#error("Unsupported Platform") +#endif + +// MARK: - Public typealiases + +// FIXME: `CModeT` ought to be deprecated and replaced with `CInterop.Mode` +// if/when the compiler becomes less strict about availability checking +// of "namespaced" typealiases. (rdar://81722893) +#if os(Windows) +/// The C `mode_t` type. +public typealias CModeT = CInt +#else +/// The C `mode_t` type. +@available(System 0.0.1, *) +public typealias CModeT = mode_t +#endif + +/// A namespace for C and platform types +@available(System 0.0.2, *) +public enum CInterop { +#if os(Windows) + public typealias Mode = CInt +#else + public typealias Mode = mode_t +#endif + + /// The C `char` type + public typealias Char = CChar + + #if os(Windows) + /// The platform's preferred character type. On Unix, this is an 8-bit C + /// `char` (which may be signed or unsigned, depending on platform). On + /// Windows, this is `UInt16` (a "wide" character). + public typealias PlatformChar = UInt16 + #else + /// The platform's preferred character type. On Unix, this is an 8-bit C + /// `char` (which may be signed or unsigned, depending on platform). On + /// Windows, this is `UInt16` (a "wide" character). + public typealias PlatformChar = CInterop.Char + #endif + + #if os(Windows) + /// The platform's preferred Unicode encoding. On Unix this is UTF-8 and on + /// Windows it is UTF-16. Native strings may contain invalid Unicode, + /// which will be handled by either error-correction or failing, depending + /// on API. + public typealias PlatformUnicodeEncoding = UTF16 + #else + /// The platform's preferred Unicode encoding. On Unix this is UTF-8 and on + /// Windows it is UTF-16. Native strings may contain invalid Unicode, + /// which will be handled by either error-correction or failing, depending + /// on API. + public typealias PlatformUnicodeEncoding = UTF8 + #endif +} + +#if !os(Windows) +@available(System 99, *) +extension CInterop { + public typealias Stat = stat + public typealias DeviceID = dev_t + public typealias Inode = ino_t + public typealias UserID = uid_t + public typealias GroupID = gid_t + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + public typealias FileFlags = UInt32 + #endif +} +#endif diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift new file mode 100644 index 00000000..3e71ec90 --- /dev/null +++ b/Sources/System/Internals/Constants.swift @@ -0,0 +1,784 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2024 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 + */ + +// For platform constants redefined in Swift. We define them here so that +// they can be used anywhere without imports and without confusion to +// unavailable local decls. + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif os(Windows) +import CSystem +import ucrt +#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 + +// MARK: errno +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _ERRNO_NOT_USED: CInt { 0 } +#endif + +@_alwaysEmitIntoClient +internal var _EPERM: CInt { EPERM } + +@_alwaysEmitIntoClient +internal var _ENOENT: CInt { ENOENT } + +@_alwaysEmitIntoClient +internal var _ESRCH: CInt { ESRCH } + +@_alwaysEmitIntoClient +internal var _EINTR: CInt { EINTR } + +@_alwaysEmitIntoClient +internal var _EIO: CInt { EIO } + +@_alwaysEmitIntoClient +internal var _ENXIO: CInt { ENXIO } + +@_alwaysEmitIntoClient +internal var _E2BIG: CInt { E2BIG } + +@_alwaysEmitIntoClient +internal var _ENOEXEC: CInt { ENOEXEC } + +@_alwaysEmitIntoClient +internal var _EBADF: CInt { EBADF } + +@_alwaysEmitIntoClient +internal var _ECHILD: CInt { ECHILD } + +@_alwaysEmitIntoClient +internal var _EDEADLK: CInt { EDEADLK } + +@_alwaysEmitIntoClient +internal var _ENOMEM: CInt { ENOMEM } + +@_alwaysEmitIntoClient +internal var _EACCES: CInt { EACCES } + +@_alwaysEmitIntoClient +internal var _EFAULT: CInt { EFAULT } + +#if !os(Windows) && !os(WASI) +@_alwaysEmitIntoClient +internal var _ENOTBLK: CInt { ENOTBLK } +#endif + +@_alwaysEmitIntoClient +internal var _EBUSY: CInt { EBUSY } + +@_alwaysEmitIntoClient +internal var _EEXIST: CInt { EEXIST } + +@_alwaysEmitIntoClient +internal var _EXDEV: CInt { EXDEV } + +@_alwaysEmitIntoClient +internal var _ENODEV: CInt { ENODEV } + +@_alwaysEmitIntoClient +internal var _ENOTDIR: CInt { ENOTDIR } + +@_alwaysEmitIntoClient +internal var _EISDIR: CInt { EISDIR } + +@_alwaysEmitIntoClient +internal var _EINVAL: CInt { EINVAL } + +@_alwaysEmitIntoClient +internal var _ENFILE: CInt { ENFILE } + +@_alwaysEmitIntoClient +internal var _EMFILE: CInt { EMFILE } + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _ENOTTY: CInt { ENOTTY } + +@_alwaysEmitIntoClient +internal var _ETXTBSY: CInt { ETXTBSY } +#endif + +@_alwaysEmitIntoClient +internal var _EFBIG: CInt { EFBIG } + +@_alwaysEmitIntoClient +internal var _ENOSPC: CInt { ENOSPC } + +@_alwaysEmitIntoClient +internal var _ESPIPE: CInt { ESPIPE } + +@_alwaysEmitIntoClient +internal var _EROFS: CInt { EROFS } + +@_alwaysEmitIntoClient +internal var _EMLINK: CInt { EMLINK } + +@_alwaysEmitIntoClient +internal var _EPIPE: CInt { EPIPE } + +@_alwaysEmitIntoClient +internal var _EDOM: CInt { EDOM } + +@_alwaysEmitIntoClient +internal var _ERANGE: CInt { ERANGE } + +@_alwaysEmitIntoClient +internal var _EAGAIN: CInt { EAGAIN } + +@_alwaysEmitIntoClient +internal var _EWOULDBLOCK: CInt { +#if os(WASI) + _getConst_EWOULDBLOCK() +#else + EWOULDBLOCK +#endif +} + +@_alwaysEmitIntoClient +internal var _EINPROGRESS: CInt { EINPROGRESS } + +@_alwaysEmitIntoClient +internal var _EALREADY: CInt { EALREADY } + +@_alwaysEmitIntoClient +internal var _ENOTSOCK: CInt { ENOTSOCK } + +@_alwaysEmitIntoClient +internal var _EDESTADDRREQ: CInt { EDESTADDRREQ } + +@_alwaysEmitIntoClient +internal var _EMSGSIZE: CInt { EMSGSIZE } + +@_alwaysEmitIntoClient +internal var _EPROTOTYPE: CInt { EPROTOTYPE } + +@_alwaysEmitIntoClient +internal var _ENOPROTOOPT: CInt { ENOPROTOOPT } + +@_alwaysEmitIntoClient +internal var _EPROTONOSUPPORT: CInt { EPROTONOSUPPORT } + +#if !os(WASI) +@_alwaysEmitIntoClient +internal var _ESOCKTNOSUPPORT: CInt { +#if os(Windows) + return WSAESOCKTNOSUPPORT +#else + return ESOCKTNOSUPPORT +#endif +} +#endif + +@_alwaysEmitIntoClient +internal var _ENOTSUP: CInt { +#if os(Windows) + return WSAEOPNOTSUPP +#else + return ENOTSUP +#endif +} + +#if !os(WASI) +@_alwaysEmitIntoClient +internal var _EPFNOSUPPORT: CInt { +#if os(Windows) + return WSAEPFNOSUPPORT +#else + return EPFNOSUPPORT +#endif +} +#endif + +@_alwaysEmitIntoClient +internal var _EAFNOSUPPORT: CInt { EAFNOSUPPORT } + +@_alwaysEmitIntoClient +internal var _EADDRINUSE: CInt { EADDRINUSE } + +@_alwaysEmitIntoClient +internal var _EADDRNOTAVAIL: CInt { EADDRNOTAVAIL } + +@_alwaysEmitIntoClient +internal var _ENETDOWN: CInt { ENETDOWN } + +@_alwaysEmitIntoClient +internal var _ENETUNREACH: CInt { ENETUNREACH } + +@_alwaysEmitIntoClient +internal var _ENETRESET: CInt { ENETRESET } + +@_alwaysEmitIntoClient +internal var _ECONNABORTED: CInt { ECONNABORTED } + +@_alwaysEmitIntoClient +internal var _ECONNRESET: CInt { ECONNRESET } + +@_alwaysEmitIntoClient +internal var _ENOBUFS: CInt { ENOBUFS } + +@_alwaysEmitIntoClient +internal var _EISCONN: CInt { EISCONN } + +@_alwaysEmitIntoClient +internal var _ENOTCONN: CInt { ENOTCONN } + +#if !os(WASI) +@_alwaysEmitIntoClient +internal var _ESHUTDOWN: CInt { +#if os(Windows) + return WSAESHUTDOWN +#else + return ESHUTDOWN +#endif +} + +@_alwaysEmitIntoClient +internal var _ETOOMANYREFS: CInt { +#if os(Windows) + return WSAETOOMANYREFS +#else + return ETOOMANYREFS +#endif +} +#endif + +@_alwaysEmitIntoClient +internal var _ETIMEDOUT: CInt { ETIMEDOUT } + +@_alwaysEmitIntoClient +internal var _ECONNREFUSED: CInt { ECONNREFUSED } + +@_alwaysEmitIntoClient +internal var _ELOOP: CInt { ELOOP } + +@_alwaysEmitIntoClient +internal var _ENAMETOOLONG: CInt { ENAMETOOLONG } + +#if !os(WASI) +@_alwaysEmitIntoClient +internal var _EHOSTDOWN: CInt { +#if os(Windows) + return WSAEHOSTDOWN +#else + return EHOSTDOWN +#endif +} +#endif + +@_alwaysEmitIntoClient +internal var _EHOSTUNREACH: CInt { EHOSTUNREACH } + +@_alwaysEmitIntoClient +internal var _ENOTEMPTY: CInt { ENOTEMPTY } + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _EPROCLIM: CInt { EPROCLIM } +#endif + +#if !os(WASI) +@_alwaysEmitIntoClient +internal var _EUSERS: CInt { +#if os(Windows) + return WSAEUSERS +#else + return EUSERS +#endif +} +#endif + +@_alwaysEmitIntoClient +internal var _EDQUOT: CInt { +#if os(Windows) + return WSAEDQUOT +#else + return EDQUOT +#endif +} + +@_alwaysEmitIntoClient +internal var _ESTALE: CInt { +#if os(Windows) + return WSAESTALE +#else + return ESTALE +#endif +} + +#if !os(WASI) +@_alwaysEmitIntoClient +internal var _EREMOTE: CInt { +#if os(Windows) + return WSAEREMOTE +#else + return EREMOTE +#endif +} +#endif + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _EBADRPC: CInt { EBADRPC } + +@_alwaysEmitIntoClient +internal var _ERPCMISMATCH: CInt { ERPCMISMATCH } + +@_alwaysEmitIntoClient +internal var _EPROGUNAVAIL: CInt { EPROGUNAVAIL } + +@_alwaysEmitIntoClient +internal var _EPROGMISMATCH: CInt { EPROGMISMATCH } + +@_alwaysEmitIntoClient +internal var _EPROCUNAVAIL: CInt { EPROCUNAVAIL } +#endif + +@_alwaysEmitIntoClient +internal var _ENOLCK: CInt { ENOLCK } + +@_alwaysEmitIntoClient +internal var _ENOSYS: CInt { ENOSYS } + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _EFTYPE: CInt { EFTYPE } + +@_alwaysEmitIntoClient +internal var _EAUTH: CInt { EAUTH } + +@_alwaysEmitIntoClient +internal var _ENEEDAUTH: CInt { ENEEDAUTH } +#endif + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _EPWROFF: CInt { EPWROFF } + +@_alwaysEmitIntoClient +internal var _EDEVERR: CInt { EDEVERR } +#endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _EOVERFLOW: CInt { EOVERFLOW } +#endif + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _EBADEXEC: CInt { EBADEXEC } + +@_alwaysEmitIntoClient +internal var _EBADARCH: CInt { EBADARCH } + +@_alwaysEmitIntoClient +internal var _ESHLIBVERS: CInt { ESHLIBVERS } + +@_alwaysEmitIntoClient +internal var _EBADMACHO: CInt { EBADMACHO } +#endif + +@_alwaysEmitIntoClient +internal var _ECANCELED: CInt { ECANCELED } + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _EIDRM: CInt { EIDRM } + +@_alwaysEmitIntoClient +internal var _ENOMSG: CInt { ENOMSG } +#endif + +@_alwaysEmitIntoClient +internal var _EILSEQ: CInt { EILSEQ } + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _ENOATTR: CInt { ENOATTR } +#endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _EBADMSG: CInt { EBADMSG } + +#if !os(OpenBSD) +@_alwaysEmitIntoClient +internal var _EMULTIHOP: CInt { EMULTIHOP } + +#if !os(WASI) && !os(FreeBSD) +@_alwaysEmitIntoClient +internal var _ENODATA: CInt { ENODATA } +#endif + +@_alwaysEmitIntoClient +internal var _ENOLINK: CInt { ENOLINK } + +#if !os(WASI) && !os(FreeBSD) +@_alwaysEmitIntoClient +internal var _ENOSR: CInt { ENOSR } + +@_alwaysEmitIntoClient +internal var _ENOSTR: CInt { ENOSTR } +#endif +#endif + +@_alwaysEmitIntoClient +internal var _EPROTO: CInt { EPROTO } + +#if !os(OpenBSD) && !os(WASI) && !os(FreeBSD) +@_alwaysEmitIntoClient +internal var _ETIME: CInt { ETIME } +#endif +#endif + + +@_alwaysEmitIntoClient +internal var _EOPNOTSUPP: CInt { +#if os(WASI) + _getConst_EOPNOTSUPP() +#else + EOPNOTSUPP +#endif +} + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _ENOPOLICY: CInt { ENOPOLICY } +#endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _ENOTRECOVERABLE: CInt { ENOTRECOVERABLE } + +@_alwaysEmitIntoClient +internal var _EOWNERDEAD: CInt { EOWNERDEAD } +#endif + +#if os(FreeBSD) +@_alwaysEmitIntoClient +internal var _ENOTCAPABLE: CInt { ENOTCAPABLE } + +@_alwaysEmitIntoClient +internal var _ECAPMODE: CInt { ECAPMODE } + +@_alwaysEmitIntoClient +internal var _EINTEGRITY: CInt { EINTEGRITY } +#endif + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _EQFULL: CInt { EQFULL } +#endif + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _ELAST: CInt { ELAST } +#endif + +// MARK: File Operations + +@_alwaysEmitIntoClient +internal var _O_RDONLY: CInt { O_RDONLY } + +@_alwaysEmitIntoClient +internal var _O_WRONLY: CInt { O_WRONLY } + +@_alwaysEmitIntoClient +internal var _O_RDWR: CInt { O_RDWR } + +#if !os(Windows) +#if canImport(Musl) +internal var _O_ACCMODE: CInt { 0x03|O_SEARCH } +#else +// TODO: API? +@_alwaysEmitIntoClient +internal var _O_ACCMODE: CInt { +#if os(WASI) + _getConst_O_ACCMODE() +#else + O_ACCMODE +#endif +} +#endif + +@_alwaysEmitIntoClient +internal var _O_NONBLOCK: CInt { +#if os(WASI) + _getConst_O_NONBLOCK() +#else + O_NONBLOCK +#endif +} +#endif + +@_alwaysEmitIntoClient +internal var _O_APPEND: CInt { +#if os(WASI) + _getConst_O_APPEND() +#else + O_APPEND +#endif +} + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _O_SHLOCK: CInt { O_SHLOCK } + +@_alwaysEmitIntoClient +internal var _O_EXLOCK: CInt { O_EXLOCK } +#endif + +#if !os(Windows) +#if !os(WASI) +// TODO: API? +@_alwaysEmitIntoClient +internal var _O_ASYNC: CInt { O_ASYNC } +#endif + +@_alwaysEmitIntoClient +internal var _O_NOFOLLOW: CInt { O_NOFOLLOW } +#endif + +#if os(FreeBSD) +@_alwaysEmitIntoClient +internal var _O_FSYNC: CInt { O_FSYNC } + +@_alwaysEmitIntoClient +internal var _O_SYNC: CInt { O_SYNC } +#endif + +@_alwaysEmitIntoClient +internal var _O_CREAT: CInt { +#if os(WASI) + _getConst_O_CREAT() +#else + O_CREAT +#endif +} + +@_alwaysEmitIntoClient +internal var _O_TRUNC: CInt { +#if os(WASI) + _getConst_O_TRUNC() +#else + O_TRUNC +#endif +} + +@_alwaysEmitIntoClient +internal var _O_EXCL: CInt { +#if os(WASI) + _getConst_O_EXCL() +#else + O_EXCL +#endif +} + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _O_EVTONLY: CInt { O_EVTONLY } +#endif + +#if !os(Windows) +// TODO: API? +@_alwaysEmitIntoClient +internal var _O_NOCTTY: CInt { O_NOCTTY } + +@_alwaysEmitIntoClient +internal var _O_DIRECTORY: CInt { +#if os(WASI) + _getConst_O_DIRECTORY() +#else + O_DIRECTORY +#endif +} +#endif + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _O_SYMLINK: CInt { O_SYMLINK } +#endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _O_CLOEXEC: CInt { O_CLOEXEC } +#endif + +@_alwaysEmitIntoClient +internal var _SEEK_SET: CInt { SEEK_SET } + +@_alwaysEmitIntoClient +internal var _SEEK_CUR: CInt { SEEK_CUR } + +@_alwaysEmitIntoClient +internal var _SEEK_END: CInt { SEEK_END } + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _SEEK_HOLE: CInt { SEEK_HOLE } + +@_alwaysEmitIntoClient +internal var _SEEK_DATA: CInt { SEEK_DATA } +#endif + +// MARK: - File System + +#if !os(Windows) + +@_alwaysEmitIntoClient +internal var _AT_FDCWD: CInt { AT_FDCWD } + +// MARK: - fstatat Flags + +@_alwaysEmitIntoClient +internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_NOFOLLOW } + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } +#endif + +#if canImport(Darwin, _version: 346) || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } +#endif + +// MARK: - File Mode / File Type + +@_alwaysEmitIntoClient +internal var _MODE_FILETYPE_MASK: mode_t { S_IFMT } + +@_alwaysEmitIntoClient +internal var _MODE_PERMISSIONS_MASK: mode_t { 0o7777 } + +@_alwaysEmitIntoClient +internal var _S_IFDIR: mode_t { S_IFDIR } + +@_alwaysEmitIntoClient +internal var _S_IFCHR: mode_t { S_IFCHR } + +@_alwaysEmitIntoClient +internal var _S_IFBLK: mode_t { S_IFBLK } + +@_alwaysEmitIntoClient +internal var _S_IFREG: mode_t { S_IFREG } + +@_alwaysEmitIntoClient +internal var _S_IFIFO: mode_t { S_IFIFO } + +@_alwaysEmitIntoClient +internal var _S_IFLNK: mode_t { S_IFLNK } + +@_alwaysEmitIntoClient +internal var _S_IFSOCK: mode_t { S_IFSOCK } + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _S_IFWHT: mode_t { S_IFWHT } +#endif + +// MARK: - stat/chflags File Flags + +// MARK: Flags Available on Darwin, FreeBSD, and OpenBSD + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) +@_alwaysEmitIntoClient +internal var _UF_NODUMP: UInt32 { UInt32(bitPattern: UF_NODUMP) } + +@_alwaysEmitIntoClient +internal var _UF_IMMUTABLE: UInt32 { UInt32(bitPattern: UF_IMMUTABLE) } + +@_alwaysEmitIntoClient +internal var _UF_APPEND: UInt32 { UInt32(bitPattern: UF_APPEND) } + +@_alwaysEmitIntoClient +internal var _SF_ARCHIVED: UInt32 { UInt32(bitPattern: SF_ARCHIVED) } + +@_alwaysEmitIntoClient +internal var _SF_IMMUTABLE: UInt32 { UInt32(bitPattern: SF_IMMUTABLE) } + +@_alwaysEmitIntoClient +internal var _SF_APPEND: UInt32 { UInt32(bitPattern: SF_APPEND) } +#endif + +// MARK: Flags Available on Darwin and FreeBSD + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _UF_OPAQUE: UInt32 { UInt32(bitPattern: UF_OPAQUE) } + +@_alwaysEmitIntoClient +internal var _UF_HIDDEN: UInt32 { UInt32(bitPattern: UF_HIDDEN) } + +@_alwaysEmitIntoClient +internal var _SF_NOUNLINK: UInt32 { UInt32(bitPattern: SF_NOUNLINK) } +#endif + +// MARK: Flags Available on Darwin Only + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _UF_COMPRESSED: UInt32 { UInt32(bitPattern: UF_COMPRESSED) } + +@_alwaysEmitIntoClient +internal var _UF_TRACKED: UInt32 { UInt32(bitPattern: UF_TRACKED) } + +@_alwaysEmitIntoClient +internal var _UF_DATAVAULT: UInt32 { UInt32(bitPattern: UF_DATAVAULT) } + +@_alwaysEmitIntoClient +internal var _SF_RESTRICTED: UInt32 { UInt32(bitPattern: SF_RESTRICTED) } + +@_alwaysEmitIntoClient +internal var _SF_FIRMLINK: UInt32 { UInt32(bitPattern: SF_FIRMLINK) } + +@_alwaysEmitIntoClient +internal var _SF_DATALESS: UInt32 { UInt32(bitPattern: SF_DATALESS) } +#endif + +// MARK: Flags Available on FreeBSD Only + +#if os(FreeBSD) +@_alwaysEmitIntoClient +internal var _UF_NOUNLINK: UInt32 { UInt32(bitPattern: UF_NOUNLINK) } + +@_alwaysEmitIntoClient +internal var _UF_OFFLINE: UInt32 { UInt32(bitPattern: UF_OFFLINE) } + +@_alwaysEmitIntoClient +internal var _UF_READONLY: UInt32 { UInt32(bitPattern: UF_READONLY) } + +@_alwaysEmitIntoClient +internal var _UF_REPARSE: UInt32 { UInt32(bitPattern: UF_REPARSE) } + +@_alwaysEmitIntoClient +internal var _UF_SPARSE: UInt32 { UInt32(bitPattern: UF_SPARSE) } + +@_alwaysEmitIntoClient +internal var _UF_SYSTEM: UInt32 { UInt32(bitPattern: UF_SYSTEM) } + +@_alwaysEmitIntoClient +internal var _SF_SNAPSHOT: UInt32 { UInt32(bitPattern: SF_SNAPSHOT) } +#endif + +#endif // !os(Windows) diff --git a/Sources/System/Internals/Exports.swift b/Sources/System/Internals/Exports.swift new file mode 100644 index 00000000..15ee45c3 --- /dev/null +++ b/Sources/System/Internals/Exports.swift @@ -0,0 +1,242 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2024 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 + */ + +// Internal wrappers and typedefs which help reduce #if littering in System's +// code base. + +// TODO: Should CSystem just include all the header files we need? + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif os(Windows) +import CSystem +import ucrt +#elseif canImport(Glibc) +@_implementationOnly import CSystem +import Glibc +#elseif canImport(Musl) +@_implementationOnly import CSystem +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Android) +@_implementationOnly import CSystem +import Android +#else +#error("Unsupported Platform") +#endif + +internal typealias _COffT = off_t + +// MARK: syscalls and variables + +#if SYSTEM_PACKAGE_DARWIN +internal var system_errno: CInt { + get { Darwin.errno } + set { Darwin.errno = newValue } +} +#elseif os(Windows) +internal var system_errno: CInt { + get { + var value: CInt = 0 + // TODO(compnerd) handle the error? + _ = ucrt._get_errno(&value) + return value + } + set { + _ = ucrt._set_errno(newValue) + } +} +#elseif canImport(Glibc) +internal var system_errno: CInt { + get { Glibc.errno } + set { Glibc.errno = newValue } +} +#elseif canImport(Musl) +internal var system_errno: CInt { + get { Musl.errno } + set { Musl.errno = newValue } +} +#elseif canImport(WASILibc) +internal var system_errno: CInt { + get { WASILibc.errno } + set { WASILibc.errno = newValue } +} +#elseif canImport(Android) +internal var system_errno: CInt { + get { Android.errno } + set { Android.errno = newValue } +} +#endif + +// MARK: C stdlib decls + +// Convention: `system_foo` is system's wrapper for `foo`. + +internal func system_strerror(_ __errnum: Int32) -> UnsafeMutablePointer! { + strerror(__errnum) +} + +internal func system_strlen(_ s: UnsafePointer) -> Int { + strlen(s) +} +internal func system_strlen(_ s: UnsafeMutablePointer) -> Int { + strlen(s) +} + +#if !os(Windows) +@available(System 99, *) +internal func system_stat(_ p: UnsafePointer, _ s: inout CInterop.Stat) -> Int32 { + stat(p, &s) +} +@available(System 99, *) +internal func system_lstat(_ p: UnsafePointer, _ s: inout CInterop.Stat) -> Int32 { + lstat(p, &s) +} +@available(System 99, *) +internal func system_fstat(_ fd: CInt, _ s: inout CInterop.Stat) -> Int32 { + fstat(fd, &s) +} +@available(System 99, *) +internal func system_fstatat(_ fd: CInt, _ p: UnsafePointer, _ s: inout CInterop.Stat, _ flags: CInt) -> Int32 { + fstatat(fd, p, &s, flags) +} +#endif + +// Convention: `system_platform_foo` is a +// platform-representation-abstracted wrapper around `foo`-like functionality. +// Type and layout differences such as the `char` vs `wchar` are abstracted. +// + +// strlen for the platform string +internal func system_platform_strlen(_ s: UnsafePointer) -> Int { + #if os(Windows) + return wcslen(s) + #else + return strlen(s) + #endif +} + +// memset for raw buffers +// FIXME: Do we really not have something like this in the stdlib already? +internal func system_memset( + _ buffer: UnsafeMutableRawBufferPointer, + to byte: UInt8 +) { + guard buffer.count > 0 else { return } + memset(buffer.baseAddress!, CInt(byte), buffer.count) +} + +// Interop between String and platfrom string +extension String { + internal func _withPlatformString( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + // Need to #if because CChar may be signed + #if os(Windows) + return try withCString(encodedAs: CInterop.PlatformUnicodeEncoding.self, body) + #else + return try withCString(body) + #endif + } + + internal init?(_platformString platformString: UnsafePointer) { + // Need to #if because CChar may be signed + #if os(Windows) + guard let strRes = String.decodeCString( + platformString, + as: CInterop.PlatformUnicodeEncoding.self, + repairingInvalidCodeUnits: false + ) else { return nil } + assert(strRes.repairsMade == false) + self = strRes.result + return + + #else + self.init(validatingCString: platformString) + #endif + } + + internal init( + _errorCorrectingPlatformString platformString: UnsafePointer + ) { + // Need to #if because CChar may be signed + #if os(Windows) + let strRes = String.decodeCString( + platformString, + as: CInterop.PlatformUnicodeEncoding.self, + repairingInvalidCodeUnits: true) + self = strRes!.result + return + #else + self.init(cString: platformString) + #endif + } +} + +// TLS +#if os(Windows) +internal typealias _PlatformTLSKey = DWORD +#elseif os(WASI) && (swift(<6.1) || !_runtime(_multithreaded)) +// Mock TLS storage for single-threaded WASI +internal final class _PlatformTLSKey { + fileprivate init() {} +} +private final class TLSStorage: @unchecked Sendable { + var storage = [ObjectIdentifier: UnsafeMutableRawPointer]() +} +private let sharedTLSStorage = TLSStorage() + +func pthread_setspecific(_ key: _PlatformTLSKey, _ p: UnsafeMutableRawPointer?) -> Int { + sharedTLSStorage.storage[ObjectIdentifier(key)] = p + return 0 +} + +func pthread_getspecific(_ key: _PlatformTLSKey) -> UnsafeMutableRawPointer? { + sharedTLSStorage.storage[ObjectIdentifier(key)] +} +#else +internal typealias _PlatformTLSKey = pthread_key_t +#endif + +internal func makeTLSKey() -> _PlatformTLSKey { + #if os(Windows) + let raw: DWORD = FlsAlloc(nil) + if raw == FLS_OUT_OF_INDEXES { + fatalError("Unable to create key") + } + return raw + #elseif os(WASI) && (swift(<6.1) || !_runtime(_multithreaded)) + return _PlatformTLSKey() + #else + var raw = pthread_key_t() + guard 0 == pthread_key_create(&raw, nil) else { + fatalError("Unable to create key") + } + return raw + #endif +} +internal func setTLS(_ key: _PlatformTLSKey, _ p: UnsafeMutableRawPointer?) { + #if os(Windows) + guard FlsSetValue(key, p) else { + fatalError("Unable to set TLS") + } + #else + guard 0 == pthread_setspecific(key, p) else { + fatalError("Unable to set TLS") + } + #endif +} +internal func getTLS(_ key: _PlatformTLSKey) -> UnsafeMutableRawPointer? { + #if os(Windows) + return FlsGetValue(key) + #else + return pthread_getspecific(key) + #endif +} diff --git a/Sources/System/Internals/Mocking.swift b/Sources/System/Internals/Mocking.swift new file mode 100644 index 00000000..ffdaaa92 --- /dev/null +++ b/Sources/System/Internals/Mocking.swift @@ -0,0 +1,209 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +// Syscall mocking support. +// +// NOTE: This is currently the bare minimum needed for System's testing purposes, though we do +// eventually want to expose some solution to users. +// +// Mocking is contextual, accessible through MockingDriver.withMockingEnabled. Mocking +// state, including whether it is enabled, is stored in thread-local storage. Mocking is only +// enabled in testing builds of System currently, to minimize runtime overhead of release builds. +// + +#if ENABLE_MOCKING +internal struct Trace { + internal struct Entry { + + internal var name: String + internal var arguments: [AnyHashable] + + internal init(name: String, _ arguments: [AnyHashable]) { + self.name = name + self.arguments = arguments + } + } + + private var entries: [Entry] = [] + private var firstEntry: Int = 0 + + internal var isEmpty: Bool { firstEntry >= entries.count } + + internal mutating func dequeue() -> Entry? { + guard !self.isEmpty else { return nil } + defer { firstEntry += 1 } + return entries[firstEntry] + } + + fileprivate mutating func add(_ e: Entry) { + entries.append(e) + } +} + +internal enum ForceErrno: Equatable { + case none + case always(errno: CInt) + + case counted(errno: CInt, count: Int) +} + +// Provide access to the driver, context, and trace stack of mocking +internal class MockingDriver { + // Record syscalls and their arguments + internal var trace = Trace() + + // Mock errors inside syscalls + internal var forceErrno = ForceErrno.none + + // Whether we should pretend to be Windows for syntactic operations + // inside FilePath + fileprivate var forceWindowsSyntaxForPaths: Bool? = nil +} + +private let driverKey: _PlatformTLSKey = { makeTLSKey() }() + +internal var currentMockingDriver: MockingDriver? { + #if !ENABLE_MOCKING + fatalError("Contextual mocking in non-mocking build") + #endif + + guard let rawPtr = getTLS(driverKey) else { return nil } + + return Unmanaged.fromOpaque(rawPtr).takeUnretainedValue() +} + +extension MockingDriver { + /// Enables mocking for the duration of `f` with a clean trace queue + /// Restores prior mocking status and trace queue after execution + internal static func withMockingEnabled( + _ f: (MockingDriver) throws -> () + ) rethrows { + let priorMocking = currentMockingDriver + let driver = MockingDriver() + + defer { + if let object = priorMocking { + setTLS(driverKey, Unmanaged.passUnretained(object).toOpaque()) + } else { + setTLS(driverKey, nil) + } + _fixLifetime(driver) + } + + setTLS(driverKey, Unmanaged.passUnretained(driver).toOpaque()) + return try f(driver) + } +} + +// Check TLS for mocking +@inline(never) +private var contextualMockingEnabled: Bool { + return currentMockingDriver != nil +} + +extension MockingDriver { + internal static var enabled: Bool { mockingEnabled } + + internal static var forceWindowsPaths: Bool? { + currentMockingDriver?.forceWindowsSyntaxForPaths + } +} + +#endif // ENABLE_MOCKING + +@inline(__always) +internal var mockingEnabled: Bool { + // Fast constant-foldable check for release builds + #if ENABLE_MOCKING + return contextualMockingEnabled + #else + return false + #endif +} + +@inline(__always) +internal var forceWindowsPaths: Bool? { + #if !ENABLE_MOCKING + return nil + #else + return MockingDriver.forceWindowsPaths + #endif +} + + +#if ENABLE_MOCKING +// Strip the mock_system prefix and the arg list suffix +private func originalSyscallName(_ function: String) -> String { + // `function` must be of format `system_()` + precondition(function.starts(with: "system_")) + return String(function.dropFirst("system_".count).prefix { $0 != "(" }) +} + +private func mockImpl( + name: String, + path: UnsafePointer?, + _ args: [AnyHashable] +) -> CInt { + precondition(mockingEnabled) + let origName = originalSyscallName(name) + guard let driver = currentMockingDriver else { + fatalError("Mocking requested from non-mocking context") + } + var mockArgs: Array = [] + if let p = path { + mockArgs.append(String(_errorCorrectingPlatformString: p)) + } + mockArgs.append(contentsOf: args) + driver.trace.add(Trace.Entry(name: origName, mockArgs)) + + switch driver.forceErrno { + case .none: break + case .always(let e): + system_errno = e + return -1 + case .counted(let e, let count): + assert(count >= 1) + system_errno = e + driver.forceErrno = count > 1 ? .counted(errno: e, count: count-1) : .none + return -1 + } + + return 0 +} + +internal func _mock( + name: String = #function, path: UnsafePointer? = nil, _ args: AnyHashable... +) -> CInt { + return mockImpl(name: name, path: path, args) +} +internal func _mockInt( + name: String = #function, path: UnsafePointer? = nil, _ args: AnyHashable... +) -> Int { + Int(mockImpl(name: name, path: path, args)) +} + +internal func _mockOffT( + name: String = #function, path: UnsafePointer? = nil, _ args: AnyHashable... +) -> _COffT { + _COffT(mockImpl(name: name, path: path, args)) +} +#endif // ENABLE_MOCKING + +// Force paths to be treated as Windows syntactically if `enabled` is +// true, and as POSIX syntactically if not. +internal func _withWindowsPaths(enabled: Bool, _ body: () -> ()) { + #if ENABLE_MOCKING + MockingDriver.withMockingEnabled { driver in + driver.forceWindowsSyntaxForPaths = enabled + body() + } + #else + body() + #endif +} diff --git a/Sources/System/Internals/RawBuffer.swift b/Sources/System/Internals/RawBuffer.swift new file mode 100644 index 00000000..83161c47 --- /dev/null +++ b/Sources/System/Internals/RawBuffer.swift @@ -0,0 +1,108 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 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 +*/ + +// A copy-on-write fixed-size buffer of raw memory. +internal struct _RawBuffer { + internal var _storage: Storage? + + internal init() { + self._storage = nil + } + + internal init(minimumCapacity: Int) { + if minimumCapacity > 0 { + self._storage = Storage.create(minimumCapacity: minimumCapacity) + } else { + self._storage = nil + } + } +} + +extension _RawBuffer { + internal var capacity: Int { + _storage?.header ?? 0 // Note: not capacity! + } + + internal mutating func ensureUnique() { + guard _storage != nil else { return } + let unique = isKnownUniquelyReferenced(&_storage) + if !unique { + _storage = _copy(capacity: capacity) + } + } + + internal func _grow(desired: Int) -> Int { + let next = Int(1.75 * Double(self.capacity)) + return Swift.max(next, desired) + } + + internal mutating func ensureUnique(capacity: Int) { + let unique = isKnownUniquelyReferenced(&_storage) + if !unique || self.capacity < capacity { + _storage = _copy(capacity: _grow(desired: capacity)) + } + } + + internal func withUnsafeBytes( + _ body: (UnsafeRawBufferPointer) throws -> R + ) rethrows -> R { + guard let storage = _storage else { + return try body(UnsafeRawBufferPointer(start: nil, count: 0)) + } + return try storage.withUnsafeMutablePointers { count, bytes in + let buffer = UnsafeRawBufferPointer(start: bytes, count: count.pointee) + return try body(buffer) + } + } + + internal mutating func withUnsafeMutableBytes( + _ body: (UnsafeMutableRawBufferPointer) throws -> R + ) rethrows -> R { + guard _storage != nil else { + return try body(UnsafeMutableRawBufferPointer(start: nil, count: 0)) + } + ensureUnique() + return try _storage!.withUnsafeMutablePointers { count, bytes in + let buffer = UnsafeMutableRawBufferPointer(start: bytes, count: count.pointee) + return try body(buffer) + } + } +} + +extension _RawBuffer { + internal class Storage: ManagedBuffer { + internal static func create(minimumCapacity: Int) -> Storage { + Storage.create( + minimumCapacity: minimumCapacity, + makingHeaderWith: { +#if os(OpenBSD) + minimumCapacity +#else + $0.capacity +#endif + } + ) as! Storage + } + } + + internal func _copy(capacity: Int) -> Storage { + let copy = Storage.create(minimumCapacity: capacity) + copy.withUnsafeMutablePointers { dstlen, dst in + self.withUnsafeBytes { src in + guard src.count > 0 else { return } + assert(src.count <= dstlen.pointee) + UnsafeMutableRawPointer(dst) + .copyMemory( + from: src.baseAddress!, + byteCount: Swift.min(src.count, dstlen.pointee)) + } + } + return copy + } +} diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift new file mode 100644 index 00000000..f6eb5339 --- /dev/null +++ b/Sources/System/Internals/Syscalls.swift @@ -0,0 +1,271 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2025 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WASILibc) +import CSystem +import WASILibc +#elseif os(Windows) +import ucrt +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + +// Interacting with the mocking system, tracing, etc., is a potentially significant +// amount of code size, so we hand outline that code for every syscall + +// open +internal func system_open( + _ path: UnsafePointer, _ oflag: Int32 +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(path: path, oflag) + } +#endif + return open(path, oflag) +} + +internal func system_open( + _ path: UnsafePointer, + _ oflag: Int32, _ mode: CInterop.Mode +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(path: path, oflag, mode) + } +#endif + return open(path, oflag, mode) +} + +// close +internal func system_close(_ fd: Int32) -> Int32 { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd) } +#endif + return close(fd) +} + +// read +internal func system_read( + _ fd: Int32, _ buf: UnsafeMutableRawPointer?, _ nbyte: Int +) -> Int { +#if ENABLE_MOCKING + if mockingEnabled { return _mockInt(fd, buf, nbyte) } +#endif + return read(fd, buf, nbyte) +} + +// pread +internal func system_pread( + _ fd: Int32, _ buf: UnsafeMutableRawPointer?, _ nbyte: Int, _ offset: off_t +) -> Int { +#if ENABLE_MOCKING + if mockingEnabled { return _mockInt(fd, buf, nbyte, offset) } +#endif +#if os(Android) + var zero = UInt8.zero + return withUnsafeMutablePointer(to: &zero) { + // this pread has a non-nullable `buf` pointer + pread(fd, buf ?? UnsafeMutableRawPointer($0), nbyte, offset) + } +#else + return pread(fd, buf, nbyte, offset) +#endif +} + +// lseek +internal func system_lseek( + _ fd: Int32, _ off: off_t, _ whence: Int32 +) -> off_t { +#if ENABLE_MOCKING + if mockingEnabled { return _mockOffT(fd, off, whence) } +#endif + return lseek(fd, off, whence) +} + +// write +internal func system_write( + _ fd: Int32, _ buf: UnsafeRawPointer?, _ nbyte: Int +) -> Int { +#if ENABLE_MOCKING + if mockingEnabled { return _mockInt(fd, buf, nbyte) } +#endif + return write(fd, buf, nbyte) +} + +// pwrite +internal func system_pwrite( + _ fd: Int32, _ buf: UnsafeRawPointer?, _ nbyte: Int, _ offset: off_t +) -> Int { +#if ENABLE_MOCKING + if mockingEnabled { return _mockInt(fd, buf, nbyte, offset) } +#endif +#if os(Android) + var zero = UInt8.zero + return withUnsafeMutablePointer(to: &zero) { + // this pwrite has a non-nullable `buf` pointer + pwrite(fd, buf ?? UnsafeRawPointer($0), nbyte, offset) + } +#else + return pwrite(fd, buf, nbyte, offset) +#endif +} + +#if !os(WASI) +internal func system_dup(_ fd: Int32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd) } + #endif + return dup(fd) +} + +internal func system_dup2(_ fd: Int32, _ fd2: Int32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, fd2) } + #endif + return dup2(fd, fd2) +} +#endif + +#if !os(WASI) +internal func system_pipe(_ fds: UnsafeMutablePointer) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fds) } +#endif + return pipe(fds) +} +#endif + +internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, length) } +#endif + return ftruncate(fd, length) +} + +internal func system_mkdir( + _ path: UnsafePointer, + _ mode: CInterop.Mode +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(path: path, mode) } +#endif + return mkdir(path, mode) +} + +internal func system_rmdir( + _ path: UnsafePointer +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(path: path) } +#endif + return rmdir(path) +} + +#if SYSTEM_PACKAGE_DARWIN +internal let SYSTEM_CS_DARWIN_USER_TEMP_DIR = _CS_DARWIN_USER_TEMP_DIR + +internal func system_confstr( + _ name: CInt, + _ buf: UnsafeMutablePointer, + _ len: Int +) -> Int { + return confstr(name, buf, len) +} +#endif + +#if !os(Windows) +internal let SYSTEM_AT_REMOVE_DIR = AT_REMOVEDIR +#if os(WASI) +internal let SYSTEM_DT_DIR = _getConst_DT_DIR() +internal typealias system_dirent = _system_dirent +#else +internal let SYSTEM_DT_DIR = DT_DIR +internal typealias system_dirent = dirent +#endif +#if os(Linux) || os(Android) || os(FreeBSD) || os(OpenBSD) || os(WASI) +internal typealias system_DIRPtr = OpaquePointer +#else +internal typealias system_DIRPtr = UnsafeMutablePointer +#endif + +internal func system_unlinkat( + _ fd: CInt, + _ path: UnsafePointer, + _ flag: CInt +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, path, flag) } +#endif +return unlinkat(fd, path, flag) +} + +internal func system_fdopendir( + _ fd: CInt +) -> system_DIRPtr? { + return fdopendir(fd) +} + +internal func system_readdir( + _ dir: system_DIRPtr +) -> UnsafeMutablePointer? { + #if os(WASI) + return _system_dirent_from_wasi_dirent(readdir(dir)) + #else + return readdir(dir) + #endif +} + +internal func system_rewinddir( + _ dir: system_DIRPtr +) { + return rewinddir(dir) +} + +internal func system_closedir( + _ dir: system_DIRPtr +) -> CInt { + return closedir(dir) +} + +internal func system_openat( + _ fd: CInt, + _ path: UnsafePointer, + _ oflag: Int32 +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { + return _mock(fd, path, oflag) + } +#endif + return openat(fd, path, oflag) +} +#endif + +#if !os(WASI) // WASI has no umask +internal func system_umask( + _ mode: CInterop.Mode +) -> CInterop.Mode { + return umask(mode) +} +#endif + +internal func system_getenv( + _ name: UnsafePointer +) -> UnsafeMutablePointer? { + return getenv(name) +} diff --git a/Sources/System/Internals/WindowsSyscallAdapters.swift b/Sources/System/Internals/WindowsSyscallAdapters.swift new file mode 100644 index 00000000..da5dfb8d --- /dev/null +++ b/Sources/System/Internals/WindowsSyscallAdapters.swift @@ -0,0 +1,662 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 ucrt +import WinSDK + +fileprivate var _umask: CInterop.Mode = 0o22 + +@inline(__always) +func umask( + _ mode: CInterop.Mode +) -> CInterop.Mode { + let oldMask = _umask + _umask = mode + return oldMask +} + +@inline(__always) +internal func open( + _ path: UnsafePointer, _ oflag: Int32 +) -> CInt { + let decodedFlags = DecodedOpenFlags(oflag) + + var saAttrs = SECURITY_ATTRIBUTES( + nLength: DWORD(MemoryLayout.size), + lpSecurityDescriptor: nil, + bInheritHandle: decodedFlags.bInheritHandle + ) + + guard let hFile = try? path.withCanonicalPathRepresentation({ path in + CreateFileW(path, + decodedFlags.dwDesiredAccess, + DWORD(FILE_SHARE_DELETE + | FILE_SHARE_READ + | FILE_SHARE_WRITE), + &saAttrs, + decodedFlags.dwCreationDisposition, + decodedFlags.dwFlagsAndAttributes, + nil) + }), hFile != INVALID_HANDLE_VALUE else { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + + return _open_osfhandle(intptr_t(bitPattern: hFile), oflag); +} + +@inline(__always) +internal func open( + _ path: UnsafePointer, _ oflag: Int32, + _ mode: CInterop.Mode +) -> CInt { + let actualMode = mode & ~_umask + + guard let pSD = _createSecurityDescriptor(from: actualMode, for: .file) else { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + + defer { + pSD.deallocate() + } + + let decodedFlags = DecodedOpenFlags(oflag) + + var saAttrs = SECURITY_ATTRIBUTES( + nLength: DWORD(MemoryLayout.size), + lpSecurityDescriptor: pSD, + bInheritHandle: decodedFlags.bInheritHandle + ) + + guard let hFile = try? path.withCanonicalPathRepresentation({ path in + CreateFileW(path, + decodedFlags.dwDesiredAccess, + DWORD(FILE_SHARE_DELETE + | FILE_SHARE_READ + | FILE_SHARE_WRITE), + &saAttrs, + decodedFlags.dwCreationDisposition, + decodedFlags.dwFlagsAndAttributes, + nil) + }), hFile != INVALID_HANDLE_VALUE else { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + + return _open_osfhandle(intptr_t(bitPattern: hFile), oflag); +} + +@inline(__always) +internal func close(_ fd: Int32) -> Int32 { + _close(fd) +} + +@inline(__always) +internal func lseek( + _ fd: Int32, _ off: Int64, _ whence: Int32 +) -> Int64 { + _lseeki64(fd, off, whence) +} + +@inline(__always) +internal func read( + _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int +) -> Int { + Int(_read(fd, buf, numericCast(nbyte))) +} + +@inline(__always) +internal func write( + _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int +) -> Int { + Int(_write(fd, buf, numericCast(nbyte))) +} + +@inline(__always) +internal func lseek( + _ fd: Int32, _ off: off_t, _ whence: Int32 +) -> off_t { + _lseek(fd, off, whence) +} + +@inline(__always) +internal func dup(_ fd: Int32) -> Int32 { + _dup(fd) +} + +@inline(__always) +internal func dup2(_ fd: Int32, _ fd2: Int32) -> Int32 { + // _dup2 returns 0 to indicate success. + if _dup2(fd, fd2) == 0 { + return fd2 + } + return -1 +} + +@inline(__always) +internal func pread( + _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int, _ offset: off_t +) -> Int { + let handle: intptr_t = _get_osfhandle(fd) + if handle == /* INVALID_HANDLE_VALUE */ -1 { ucrt._set_errno(EBADF); return -1 } + + // NOTE: this is a non-owning handle, do *not* call CloseHandle on it + let hFile: HANDLE = HANDLE(bitPattern: handle)! + + var ovlOverlapped: OVERLAPPED = OVERLAPPED() + ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff) + ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff) + + var nNumberOfBytesRead: DWORD = 0 + if !ReadFile(hFile, buf, DWORD(nbyte), &nNumberOfBytesRead, &ovlOverlapped) { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return Int(-1) + } + return Int(nNumberOfBytesRead) +} + +@inline(__always) +internal func pwrite( + _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int, _ offset: off_t +) -> Int { + let handle: intptr_t = _get_osfhandle(fd) + if handle == /* INVALID_HANDLE_VALUE */ -1 { ucrt._set_errno(EBADF); return -1 } + + // NOTE: this is a non-owning handle, do *not* call CloseHandle on it + let hFile: HANDLE = HANDLE(bitPattern: handle)! + + var ovlOverlapped: OVERLAPPED = OVERLAPPED() + ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff) + ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff) + + var nNumberOfBytesWritten: DWORD = 0 + if !WriteFile(hFile, buf, DWORD(nbyte), &nNumberOfBytesWritten, + &ovlOverlapped) { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return Int(-1) + } + return Int(nNumberOfBytesWritten) +} + +@inline(__always) +internal func pipe( + _ fds: UnsafeMutablePointer, bytesReserved: UInt32 = 4096 +) -> CInt { + return _pipe(fds, bytesReserved, _O_BINARY | _O_NOINHERIT); +} + +@inline(__always) +internal func ftruncate(_ fd: Int32, _ length: off_t) -> Int32 { + let handle: intptr_t = _get_osfhandle(fd) + if handle == /* INVALID_HANDLE_VALUE */ -1 { ucrt._set_errno(EBADF); return -1 } + + // NOTE: this is a non-owning handle, do *not* call CloseHandle on it + let hFile: HANDLE = HANDLE(bitPattern: handle)! + let liDesiredLength = LARGE_INTEGER(QuadPart: LONGLONG(length)) + var liCurrentOffset = LARGE_INTEGER(QuadPart: 0) + + // Save the current position and restore it when we're done + if !SetFilePointerEx(hFile, liCurrentOffset, &liCurrentOffset, + DWORD(FILE_CURRENT)) { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + defer { + _ = SetFilePointerEx(hFile, liCurrentOffset, nil, DWORD(FILE_BEGIN)); + } + + // Truncate (or extend) the file + if !SetFilePointerEx(hFile, liDesiredLength, nil, DWORD(FILE_BEGIN)) + || !SetEndOfFile(hFile) { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + + return 0; +} + +@inline(__always) +internal func mkdir( + _ path: UnsafePointer, + _ mode: CInterop.Mode +) -> CInt { + let actualMode = mode & ~_umask + + guard let pSD = _createSecurityDescriptor(from: actualMode, + for: .directory) else { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + defer { + pSD.deallocate() + } + + var saAttrs = SECURITY_ATTRIBUTES( + nLength: DWORD(MemoryLayout.size), + lpSecurityDescriptor: pSD, + bInheritHandle: false + ) + + guard (try? path.withCanonicalPathRepresentation({ path in CreateDirectoryW(path, &saAttrs) })) == true else { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + + return 0; +} + +@inline(__always) +internal func rmdir( + _ path: UnsafePointer +) -> CInt { + guard (try? path.withCanonicalPathRepresentation({ path in RemoveDirectoryW(path) })) == true else { + ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) + return -1 + } + + return 0; +} + +internal func _mapWindowsErrorToErrno(_ errorCode: DWORD) -> CInt { + switch Int32(errorCode) { + case ERROR_SUCCESS: + return 0 + case ERROR_INVALID_FUNCTION, + ERROR_INVALID_ACCESS, + ERROR_INVALID_DATA, + ERROR_INVALID_PARAMETER, + ERROR_NEGATIVE_SEEK: + return EINVAL + case ERROR_FILE_NOT_FOUND, + ERROR_PATH_NOT_FOUND, + ERROR_INVALID_DRIVE, + ERROR_NO_MORE_FILES, + ERROR_BAD_NETPATH, + ERROR_BAD_NET_NAME, + ERROR_BAD_PATHNAME, + ERROR_FILENAME_EXCED_RANGE: + return ENOENT + case ERROR_TOO_MANY_OPEN_FILES: + return EMFILE + case ERROR_ACCESS_DENIED, + ERROR_CURRENT_DIRECTORY, + ERROR_LOCK_VIOLATION, + ERROR_NETWORK_ACCESS_DENIED, + ERROR_CANNOT_MAKE, + ERROR_FAIL_I24, + ERROR_DRIVE_LOCKED, + ERROR_SEEK_ON_DEVICE, + ERROR_NOT_LOCKED, + ERROR_LOCK_FAILED, + ERROR_WRITE_PROTECT...ERROR_SHARING_BUFFER_EXCEEDED: + return EACCES + case ERROR_INVALID_HANDLE, + ERROR_INVALID_TARGET_HANDLE, + ERROR_DIRECT_ACCESS_HANDLE: + return EBADF + case ERROR_ARENA_TRASHED, + ERROR_NOT_ENOUGH_MEMORY, + ERROR_INVALID_BLOCK, + ERROR_NOT_ENOUGH_QUOTA: + return ENOMEM + case ERROR_BAD_ENVIRONMENT: + return E2BIG + case ERROR_BAD_FORMAT, + ERROR_INVALID_STARTING_CODESEG...ERROR_INFLOOP_IN_RELOC_CHAIN: + return ENOEXEC + case ERROR_NOT_SAME_DEVICE: + return EXDEV + case ERROR_FILE_EXISTS, + ERROR_ALREADY_EXISTS: + return EEXIST + case ERROR_NO_PROC_SLOTS, + ERROR_MAX_THRDS_REACHED, + ERROR_NESTING_NOT_ALLOWED: + return EAGAIN + case ERROR_BROKEN_PIPE: + return EPIPE + case ERROR_DISK_FULL: + return ENOSPC + case ERROR_WAIT_NO_CHILDREN, + ERROR_CHILD_NOT_COMPLETE: + return ECHILD + case ERROR_DIR_NOT_EMPTY: + return ENOTEMPTY + case ERROR_NO_UNICODE_TRANSLATION: + return EILSEQ + default: + return EINVAL + } +} + +fileprivate func rightsFromModeBits( + _ bits: Int, + sticky: Bool = false, + for fileOrDirectory: _FileOrDirectory +) -> DWORD { + var rights: DWORD = 0 + + if (bits & 0o4) != 0 { + rights |= DWORD(FILE_READ_ATTRIBUTES + | FILE_READ_DATA + | FILE_READ_EA + | STANDARD_RIGHTS_READ + | SYNCHRONIZE) + } + if (bits & 0o2) != 0 { + rights |= DWORD(FILE_APPEND_DATA + | FILE_WRITE_ATTRIBUTES + | FILE_WRITE_DATA + | FILE_WRITE_EA + | STANDARD_RIGHTS_WRITE + | SYNCHRONIZE) + if fileOrDirectory == .directory && !sticky { + rights |= DWORD(FILE_DELETE_CHILD) + } + } + if (bits & 0o1) != 0 { + rights |= DWORD(FILE_EXECUTE + | FILE_READ_ATTRIBUTES + | STANDARD_RIGHTS_EXECUTE + | SYNCHRONIZE) + } + + return rights +} + +fileprivate func getTokenInformation( + of: T.Type, + hToken: HANDLE, + ticTokenClass: TOKEN_INFORMATION_CLASS +) -> UnsafePointer? { + var capacity = 1024 + for _ in 0..<2 { + let buffer = UnsafeMutableRawPointer.allocate( + byteCount: capacity, + alignment: MemoryLayout.alignment + ) + + var dwLength = DWORD(0) + + if GetTokenInformation(hToken, + ticTokenClass, + buffer, + DWORD(capacity), + &dwLength) { + return UnsafePointer(buffer.assumingMemoryBound(to: T.self)) + } + + buffer.deallocate() + + capacity = Int(dwLength) + } + return nil +} + +internal enum _FileOrDirectory { + case file + case directory +} + +/// Build a SECURITY_DESCRIPTOR from UNIX-style "mode" bits. This only +/// takes account of the rwx and sticky bits; there's really nothing that +/// we can do about setuid/setgid. +internal func _createSecurityDescriptor(from mode: CInterop.Mode, + for fileOrDirectory: _FileOrDirectory) + -> PSECURITY_DESCRIPTOR? { + let ownerPerm = (Int(mode) >> 6) & 0o7 + let groupPerm = (Int(mode) >> 3) & 0o7 + let otherPerm = Int(mode) & 0o7 + + let ownerRights = rightsFromModeBits(ownerPerm, for: fileOrDirectory) + let groupRights = rightsFromModeBits(groupPerm, + sticky: (mode & 0o1000) != 0, + for: fileOrDirectory) + let otherRights = rightsFromModeBits(otherPerm, + sticky: (mode & 0o1000) != 0, + for: fileOrDirectory) + + // If group or other permissions are *more* permissive, then we need + // some DENY ACEs as well to implement the expected semantics + let ownerDenyRights = ((ownerRights ^ groupRights) & groupRights) | + ((ownerRights ^ otherRights) & otherRights) + let groupDenyRights = (groupRights ^ otherRights) & otherRights + + var SIDAuthWorld = SID_IDENTIFIER_AUTHORITY(Value: (0, 0, 0, 0, 0, 1)) + var everyone: PSID? = nil + + guard AllocateAndInitializeSid(&SIDAuthWorld, 1, + DWORD(SECURITY_WORLD_RID), + 0, 0, 0, 0, 0, 0, 0, + &everyone) else { + return nil + } + guard let everyone = everyone else { + return nil + } + defer { + FreeSid(everyone) + } + + let hToken = GetCurrentThreadEffectiveToken()! + + guard let pTokenUser = getTokenInformation(of: TOKEN_USER.self, + hToken: hToken, + ticTokenClass: TokenUser) else { + return nil + } + defer { + pTokenUser.deallocate() + } + + guard let pTokenPrimaryGroup = getTokenInformation( + of: TOKEN_PRIMARY_GROUP.self, + hToken: hToken, + ticTokenClass: TokenPrimaryGroup + ) else { + return nil + } + defer { + pTokenPrimaryGroup.deallocate() + } + + let user = pTokenUser.pointee.User.Sid! + let group = pTokenPrimaryGroup.pointee.PrimaryGroup! + + var eas = [ + EXPLICIT_ACCESS_W( + grfAccessPermissions: ownerRights, + grfAccessMode: GRANT_ACCESS, + grfInheritance: DWORD(NO_INHERITANCE), + Trustee: TRUSTEE_W( + pMultipleTrustee: nil, + MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_USER, + ptstrName: + user.assumingMemoryBound(to: CInterop.PlatformChar.self) + ) + ), + EXPLICIT_ACCESS_W( + grfAccessPermissions: groupRights, + grfAccessMode: GRANT_ACCESS, + grfInheritance: DWORD(NO_INHERITANCE), + Trustee: TRUSTEE_W( + pMultipleTrustee: nil, + MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_GROUP, + ptstrName: + group.assumingMemoryBound(to: CInterop.PlatformChar.self) + ) + ), + EXPLICIT_ACCESS_W( + grfAccessPermissions: otherRights, + grfAccessMode: GRANT_ACCESS, + grfInheritance: DWORD(NO_INHERITANCE), + Trustee: TRUSTEE_W( + pMultipleTrustee: nil, + MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_GROUP, + ptstrName: + everyone.assumingMemoryBound(to: CInterop.PlatformChar.self) + ) + ) + ] + + if ownerDenyRights != 0 { + eas.append( + EXPLICIT_ACCESS_W( + grfAccessPermissions: ownerDenyRights, + grfAccessMode: DENY_ACCESS, + grfInheritance: DWORD(NO_INHERITANCE), + Trustee: TRUSTEE_W( + pMultipleTrustee: nil, + MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_USER, + ptstrName: + user.assumingMemoryBound(to: CInterop.PlatformChar.self) + ) + ) + ) + } + + if groupDenyRights != 0 { + eas.append( + EXPLICIT_ACCESS_W( + grfAccessPermissions: groupDenyRights, + grfAccessMode: DENY_ACCESS, + grfInheritance: DWORD(NO_INHERITANCE), + Trustee: TRUSTEE_W( + pMultipleTrustee: nil, + MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_GROUP, + ptstrName: + group.assumingMemoryBound(to: CInterop.PlatformChar.self) + ) + ) + ) + } + + var pACL: PACL? = nil + guard SetEntriesInAclW(ULONG(eas.count), + &eas, + nil, + &pACL) == ERROR_SUCCESS else { + return nil + } + defer { + LocalFree(pACL) + } + + // Create the security descriptor, making sure that inherited ACEs don't + // take effect, since that wouldn't match the behaviour of mode bits. + var descriptor = SECURITY_DESCRIPTOR() + + guard InitializeSecurityDescriptor(&descriptor, + DWORD(SECURITY_DESCRIPTOR_REVISION)) else { + return nil + } + + guard SetSecurityDescriptorControl(&descriptor, + SECURITY_DESCRIPTOR_CONTROL(SE_DACL_PROTECTED), + SECURITY_DESCRIPTOR_CONTROL(SE_DACL_PROTECTED)) + && SetSecurityDescriptorOwner(&descriptor, user, false) + && SetSecurityDescriptorGroup(&descriptor, group, false) + && SetSecurityDescriptorDacl(&descriptor, + true, + pACL, + false) else { + return nil + } + + // Make it self-contained (up to this point it uses pointers) + var dwRelativeSize = DWORD(0) + + guard !MakeSelfRelativeSD(&descriptor, nil, &dwRelativeSize) + && GetLastError() == ERROR_INSUFFICIENT_BUFFER else { + return nil + } + + let pDescriptor = UnsafeMutableRawPointer.allocate( + byteCount: Int(dwRelativeSize), + alignment: MemoryLayout.alignment + ).assumingMemoryBound(to: SECURITY_DESCRIPTOR.self) + + guard MakeSelfRelativeSD(&descriptor, pDescriptor, &dwRelativeSize) else { + pDescriptor.deallocate() + return nil + } + + return UnsafeMutableRawPointer(pDescriptor) +} + +fileprivate struct DecodedOpenFlags { + var dwDesiredAccess: DWORD + var dwCreationDisposition: DWORD + var bInheritHandle: WindowsBool + var dwFlagsAndAttributes: DWORD + + init(_ oflag: Int32) { + switch oflag & (_O_CREAT | _O_EXCL | _O_TRUNC) { + case _O_CREAT | _O_EXCL, _O_CREAT | _O_EXCL | _O_TRUNC: + dwCreationDisposition = DWORD(CREATE_NEW) + case _O_CREAT: + dwCreationDisposition = DWORD(OPEN_ALWAYS) + case _O_CREAT | _O_TRUNC: + dwCreationDisposition = DWORD(CREATE_ALWAYS) + case _O_TRUNC: + dwCreationDisposition = DWORD(TRUNCATE_EXISTING) + default: + dwCreationDisposition = DWORD(OPEN_EXISTING) + } + + // The _O_RDONLY, _O_WRONLY and _O_RDWR flags are non-overlapping + // on Windows; in particular, _O_RDONLY is zero, which means we can't + // test for it by AND-ing. + dwDesiredAccess = 0 + switch (oflag & (_O_RDONLY|_O_WRONLY|_O_RDWR)) { + case _O_RDONLY: + dwDesiredAccess |= DWORD(GENERIC_READ) + case _O_WRONLY: + dwDesiredAccess |= DWORD(GENERIC_WRITE) + case _O_RDWR: + dwDesiredAccess |= DWORD(GENERIC_READ) | DWORD(GENERIC_WRITE) + default: + break + } + + bInheritHandle = WindowsBool((oflag & _O_NOINHERIT) == 0) + + dwFlagsAndAttributes = 0 + if (oflag & _O_SEQUENTIAL) != 0 { + dwFlagsAndAttributes |= DWORD(FILE_FLAG_SEQUENTIAL_SCAN) + } + if (oflag & _O_RANDOM) != 0 { + dwFlagsAndAttributes |= DWORD(FILE_FLAG_RANDOM_ACCESS) + } + if (oflag & _O_TEMPORARY) != 0 { + dwFlagsAndAttributes |= DWORD(FILE_FLAG_DELETE_ON_CLOSE) + } + + if (oflag & _O_SHORT_LIVED) != 0 { + dwFlagsAndAttributes |= DWORD(FILE_ATTRIBUTE_TEMPORARY) + } else { + dwFlagsAndAttributes |= DWORD(FILE_ATTRIBUTE_NORMAL) + } + } +} + +#endif diff --git a/Sources/System/LinuxPlatformConstants.swift b/Sources/System/LinuxPlatformConstants.swift deleted file mode 100644 index 3a197b94..00000000 --- a/Sources/System/LinuxPlatformConstants.swift +++ /dev/null @@ -1,487 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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(Linux) - -// Ugh, this is really bad. For Darwin, we can at least rely on -// these values not changing much, but in theory they could change -// per Linux flavor or version (if no ABI) - -// MARK: errno - -@_alwaysEmitIntoClient -internal var _EPERM: CInt { 1 } - -@_alwaysEmitIntoClient -internal var _ENOENT: CInt { 2 } - -@_alwaysEmitIntoClient -internal var _ESRCH: CInt { 3 } - -@_alwaysEmitIntoClient -internal var _EINTR: CInt { 4 } - -@_alwaysEmitIntoClient -internal var _EIO: CInt { 5 } - -@_alwaysEmitIntoClient -internal var _ENXIO: CInt { 6 } - -@_alwaysEmitIntoClient -internal var _E2BIG: CInt { 7 } - -@_alwaysEmitIntoClient -internal var _ENOEXEC: CInt { 8 } - -@_alwaysEmitIntoClient -internal var _EBADF: CInt { 9 } - -@_alwaysEmitIntoClient -internal var _ECHILD: CInt { 10 } - -@_alwaysEmitIntoClient -internal var _EAGAIN: CInt { 11 } - -@_alwaysEmitIntoClient -internal var _ENOMEM: CInt { 12 } - -@_alwaysEmitIntoClient -internal var _EACCES: CInt { 13 } - -@_alwaysEmitIntoClient -internal var _EFAULT: CInt { 14 } - -@_alwaysEmitIntoClient -internal var _ENOTBLK: CInt { 15 } - -@_alwaysEmitIntoClient -internal var _EBUSY: CInt { 16 } - -@_alwaysEmitIntoClient -internal var _EEXIST: CInt { 17 } - -@_alwaysEmitIntoClient -internal var _EXDEV: CInt { 18 } - -@_alwaysEmitIntoClient -internal var _ENODEV: CInt { 19 } - -@_alwaysEmitIntoClient -internal var _ENOTDIR: CInt { 20 } - -@_alwaysEmitIntoClient -internal var _EISDIR: CInt { 21 } - -@_alwaysEmitIntoClient -internal var _EINVAL: CInt { 22 } - -@_alwaysEmitIntoClient -internal var _ENFILE: CInt { 23 } - -@_alwaysEmitIntoClient -internal var _EMFILE: CInt { 24 } - -@_alwaysEmitIntoClient -internal var _ENOTTY: CInt { 25 } - -@_alwaysEmitIntoClient -internal var _ETXTBSY: CInt { 26 } - -@_alwaysEmitIntoClient -internal var _EFBIG: CInt { 27 } - -@_alwaysEmitIntoClient -internal var _ENOSPC: CInt { 28 } - -@_alwaysEmitIntoClient -internal var _ESPIPE: CInt { 29 } - -@_alwaysEmitIntoClient -internal var _EROFS: CInt { 30 } - -@_alwaysEmitIntoClient -internal var _EMLINK: CInt { 31 } - -@_alwaysEmitIntoClient -internal var _EPIPE: CInt { 32 } - -@_alwaysEmitIntoClient -internal var _EDOM: CInt { 33 } - -@_alwaysEmitIntoClient -internal var _ERANGE: CInt { 34 } - -@_alwaysEmitIntoClient -internal var _EDEADLK: CInt { 35 } - -@_alwaysEmitIntoClient -internal var _ENAMETOOLONG: CInt { 36 } - -@_alwaysEmitIntoClient -internal var _ENOLCK: CInt { 37 } - -@_alwaysEmitIntoClient -internal var _ENOSYS: CInt { 38 } - -@_alwaysEmitIntoClient -internal var _ENOTEMPTY: CInt { 39 } - -@_alwaysEmitIntoClient -internal var _ELOOP: CInt { 40 } - -@_alwaysEmitIntoClient -internal var _EWOULDBLOCK: CInt { _EAGAIN } - -@_alwaysEmitIntoClient -internal var _ENOMSG: CInt { 42 } - -@_alwaysEmitIntoClient -internal var _EIDRM: CInt { 43 } - -@_alwaysEmitIntoClient -internal var _ECHRNG: CInt { 44 } - -@_alwaysEmitIntoClient -internal var _EL2NSYNC: CInt { 45 } - -@_alwaysEmitIntoClient -internal var _EL3HLT: CInt { 46 } - -@_alwaysEmitIntoClient -internal var _EL3RST: CInt { 47 } - -@_alwaysEmitIntoClient -internal var _ELNRNG: CInt { 48 } - -@_alwaysEmitIntoClient -internal var _EUNATCH: CInt { 49 } - -@_alwaysEmitIntoClient -internal var _ENOCSI: CInt { 50 } - -@_alwaysEmitIntoClient -internal var _EL2HLT: CInt { 51 } - -@_alwaysEmitIntoClient -internal var _EBADE: CInt { 52 } - -@_alwaysEmitIntoClient -internal var _EBADR: CInt { 53 } - -@_alwaysEmitIntoClient -internal var _EXFULL: CInt { 54 } - -@_alwaysEmitIntoClient -internal var _ENOANO: CInt { 55 } - -@_alwaysEmitIntoClient -internal var _EBADRQC: CInt { 56 } - -@_alwaysEmitIntoClient -internal var _EBADSLT: CInt { 57 } - -@_alwaysEmitIntoClient -internal var _EDEADLOCK: CInt { _EDEADLK } - -@_alwaysEmitIntoClient -internal var _EBFONT: CInt { 59 } - -@_alwaysEmitIntoClient -internal var _ENOSTR: CInt { 60 } - -@_alwaysEmitIntoClient -internal var _ENODATA: CInt { 61 } - -@_alwaysEmitIntoClient -internal var _ETIME: CInt { 62 } - -@_alwaysEmitIntoClient -internal var _ENOSR: CInt { 63 } - -@_alwaysEmitIntoClient -internal var _ENONET: CInt { 64 } - -@_alwaysEmitIntoClient -internal var _ENOPKG: CInt { 65 } - -@_alwaysEmitIntoClient -internal var _EREMOTE: CInt { 66 } - -@_alwaysEmitIntoClient -internal var _ENOLINK: CInt { 67 } - -@_alwaysEmitIntoClient -internal var _EADV: CInt { 68 } - -@_alwaysEmitIntoClient -internal var _ESRMNT: CInt { 69 } - -@_alwaysEmitIntoClient -internal var _ECOMM: CInt { 70 } - -@_alwaysEmitIntoClient -internal var _EPROTO: CInt { 71 } - -@_alwaysEmitIntoClient -internal var _EMULTIHOP: CInt { 72 } - -@_alwaysEmitIntoClient -internal var _EDOTDOT: CInt { 73 } - -@_alwaysEmitIntoClient -internal var _EBADMSG: CInt { 74 } - -@_alwaysEmitIntoClient -internal var _EOVERFLOW: CInt { 75 } - -@_alwaysEmitIntoClient -internal var _ENOTUNIQ: CInt { 76 } - -@_alwaysEmitIntoClient -internal var _EBADFD: CInt { 77 } - -@_alwaysEmitIntoClient -internal var _EREMCHG: CInt { 78 } - -@_alwaysEmitIntoClient -internal var _ELIBACC: CInt { 79 } - -@_alwaysEmitIntoClient -internal var _ELIBBAD: CInt { 80 } - -@_alwaysEmitIntoClient -internal var _ELIBSCN: CInt { 81 } - -@_alwaysEmitIntoClient -internal var _ELIBMAX: CInt { 82 } - -@_alwaysEmitIntoClient -internal var _ELIBEXEC: CInt { 83 } - -@_alwaysEmitIntoClient -internal var _EILSEQ: CInt { 84 } - -@_alwaysEmitIntoClient -internal var _ERESTART: CInt { 85 } - -@_alwaysEmitIntoClient -internal var _ESTRPIPE: CInt { 86 } - -@_alwaysEmitIntoClient -internal var _EUSERS: CInt { 87 } - -@_alwaysEmitIntoClient -internal var _ENOTSOCK: CInt { 88 } - -@_alwaysEmitIntoClient -internal var _EDESTADDRREQ: CInt { 89 } - -@_alwaysEmitIntoClient -internal var _EMSGSIZE: CInt { 90 } - -@_alwaysEmitIntoClient -internal var _EPROTOTYPE: CInt { 91 } - -@_alwaysEmitIntoClient -internal var _ENOPROTOOPT: CInt { 92 } - -@_alwaysEmitIntoClient -internal var _EPROTONOSUPPORT: CInt { 93 } - -@_alwaysEmitIntoClient -internal var _ESOCKTNOSUPPORT: CInt { 94 } - -@_alwaysEmitIntoClient -internal var _EOPNOTSUPP: CInt { 95 } - -@_alwaysEmitIntoClient -internal var _ENOTSUP: CInt { _EOPNOTSUPP } - -@_alwaysEmitIntoClient -internal var _EPFNOSUPPORT: CInt { 96 } - -@_alwaysEmitIntoClient -internal var _EAFNOSUPPORT: CInt { 97 } - -@_alwaysEmitIntoClient -internal var _EADDRINUSE: CInt { 98 } - -@_alwaysEmitIntoClient -internal var _EADDRNOTAVAIL: CInt { 99 } - -@_alwaysEmitIntoClient -internal var _ENETDOWN: CInt { 100 } - -@_alwaysEmitIntoClient -internal var _ENETUNREACH: CInt { 101 } - -@_alwaysEmitIntoClient -internal var _ENETRESET: CInt { 102 } - -@_alwaysEmitIntoClient -internal var _ECONNABORTED: CInt { 103 } - -@_alwaysEmitIntoClient -internal var _ECONNRESET: CInt { 104 } - -@_alwaysEmitIntoClient -internal var _ENOBUFS: CInt { 105 } - -@_alwaysEmitIntoClient -internal var _EISCONN: CInt { 106 } - -@_alwaysEmitIntoClient -internal var _ENOTCONN: CInt { 107 } - -@_alwaysEmitIntoClient -internal var _ESHUTDOWN: CInt { 108 } - -@_alwaysEmitIntoClient -internal var _ETOOMANYREFS: CInt { 109 } - -@_alwaysEmitIntoClient -internal var _ETIMEDOUT: CInt { 110 } - -@_alwaysEmitIntoClient -internal var _ECONNREFUSED: CInt { 111 } - -@_alwaysEmitIntoClient -internal var _EHOSTDOWN: CInt { 112 } - -@_alwaysEmitIntoClient -internal var _EHOSTUNREACH: CInt { 113 } - -@_alwaysEmitIntoClient -internal var _EALREADY: CInt { 114 } - -@_alwaysEmitIntoClient -internal var _EINPROGRESS: CInt { 115 } - -@_alwaysEmitIntoClient -internal var _ESTALE: CInt { 116 } - -@_alwaysEmitIntoClient -internal var _EUCLEAN: CInt { 117 } - -@_alwaysEmitIntoClient -internal var _ENOTNAM: CInt { 118 } - -@_alwaysEmitIntoClient -internal var _ENAVAIL: CInt { 119 } - -@_alwaysEmitIntoClient -internal var _EISNAM: CInt { 120 } - -@_alwaysEmitIntoClient -internal var _EREMOTEIO: CInt { 121 } - -@_alwaysEmitIntoClient -internal var _EDQUOT: CInt { 122 } - -@_alwaysEmitIntoClient -internal var _ENOMEDIUM: CInt { 123 } - -@_alwaysEmitIntoClient -internal var _EMEDIUMTYPE: CInt { 124 } - -@_alwaysEmitIntoClient -internal var _ECANCELED: CInt { 125 } - -@_alwaysEmitIntoClient -internal var _ENOKEY: CInt { 126 } - -@_alwaysEmitIntoClient -internal var _EKEYEXPIRED: CInt { 127 } - -@_alwaysEmitIntoClient -internal var _EKEYREVOKED: CInt { 128 } - -@_alwaysEmitIntoClient -internal var _EKEYREJECTED: CInt { 129 } - -@_alwaysEmitIntoClient -internal var _EOWNERDEAD: CInt { 130 } - -@_alwaysEmitIntoClient -internal var _ENOTRECOVERABLE: CInt { 131 } - -@_alwaysEmitIntoClient -internal var _ERFKILL: CInt { 132 } - -@_alwaysEmitIntoClient -internal var _EHWPOISON: CInt { 133 } - - -// MARK: File Operations - -@_alwaysEmitIntoClient -internal var _O_ACCMODE: CInt { 0o00000003 } - -@_alwaysEmitIntoClient -internal var _O_RDONLY: CInt { 0o00000000 } - -@_alwaysEmitIntoClient -internal var _O_WRONLY: CInt { 0o00000001 } - -@_alwaysEmitIntoClient -internal var _O_RDWR: CInt { 0o00000002 } - -@_alwaysEmitIntoClient -internal var _O_CREAT: CInt { 0o00000100 } - -@_alwaysEmitIntoClient -internal var _O_EXCL: CInt { 0o00000200 } - -@_alwaysEmitIntoClient -internal var _O_NOCTTY: CInt { 0o00000400 } - -@_alwaysEmitIntoClient -internal var _O_TRUNC: CInt { 0o00001000 } - -@_alwaysEmitIntoClient -internal var _O_APPEND: CInt { 0o00002000 } - -@_alwaysEmitIntoClient -internal var _O_NONBLOCK: CInt { 0o00004000 } - -@_alwaysEmitIntoClient -internal var _O_DSYNC: CInt { 0o00010000 } - -@_alwaysEmitIntoClient -internal var _FASYNC: CInt { 0o00020000 } - -@_alwaysEmitIntoClient -internal var _O_DIRECT: CInt { 0o00040000 } - -@_alwaysEmitIntoClient -internal var _O_LARGEFILE: CInt { 0o00100000 } - -@_alwaysEmitIntoClient -internal var _O_DIRECTORY: CInt { 0o00200000 } - -@_alwaysEmitIntoClient -internal var _O_NOFOLLOW: CInt { 0o00400000 } - -@_alwaysEmitIntoClient -internal var _O_NOATIME: CInt { 0o01000000 } - -@_alwaysEmitIntoClient -internal var _O_CLOEXEC: CInt { 0o02000000 } - - -@_alwaysEmitIntoClient -internal var _SEEK_SET: CInt { 0 } - -@_alwaysEmitIntoClient -internal var _SEEK_CUR: CInt { 1 } - -@_alwaysEmitIntoClient -internal var _SEEK_END: CInt { 2 } - -#endif diff --git a/Sources/System/MachPort.swift b/Sources/System/MachPort.swift new file mode 100644 index 00000000..8204d60a --- /dev/null +++ b/Sources/System/MachPort.swift @@ -0,0 +1,377 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2022 - 2025 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +#if SYSTEM_PACKAGE_DARWIN + +import Darwin.Mach + +@available(System 1.4.0, *) +public protocol MachPortRight {} + +@available(System 1.4.0, *) +@inlinable +internal func _machPrecondition( + file: StaticString = #file, + line: UInt = #line, + _ body: @autoclosure () -> kern_return_t +) { + let kr = body() + let expected = KERN_SUCCESS + precondition(kr == expected, file: file, line: line) +} + +@available(System 1.4.0, *) +@frozen +public enum Mach { + @available(System 1.4.0, *) + public struct Port: ~Copyable { + @usableFromInline + internal var _name: mach_port_name_t + + @usableFromInline + internal var _context: mach_port_context_t + + /// Transfer ownership of an existing unmanaged Mach port right into a + /// `Mach.Port` by name. + /// + /// This initializer traps if `name` is `MACH_PORT_NULL`, or if `name` is + /// `MACH_PORT_DEAD` and the `RightType` is `Mach.ReceiveRight`. + /// + /// If the type of the right does not match the `RightType` of the + /// `Mach.Port` being constructed, behavior is undefined. + /// + /// The underlying port right will be automatically deallocated at the + /// end of the `Mach.Port` instance's lifetime. + /// + /// This initializer makes a syscall to guard the right. + public init(name: mach_port_name_t) { + precondition(name != mach_port_name_t(MACH_PORT_NULL), + "Mach.Port cannot be initialized with MACH_PORT_NULL") + self._name = name + + if RightType.self == ReceiveRight.self { + precondition( + _name != (0xFFFFFFFF as mach_port_name_t) /* MACH_PORT_DEAD */, + "Receive rights cannot be dead names" + ) + + let secret = mach_port_context_t(arc4random()) + _machPrecondition(mach_port_guard(mach_task_self_, name, secret, 0)) + self._context = secret + } + else { + self._context = 0 + } + } + + /// Borrow access to the port name in a block that can perform + /// non-consuming operations. + /// + /// Take care when using this function; many operations consume rights, + /// and send-once rights are easily consumed. + /// + /// If the right is consumed, behavior is undefined. + /// + /// The body block may optionally return something, which will then be + /// returned to the caller of withBorrowedName. + @inlinable + public func withBorrowedName( + body: (mach_port_name_t) -> ReturnType + ) -> ReturnType { + return body(_name) + } + + deinit { + if RightType.self == ReceiveRight.self { + precondition( + _name != (0xFFFFFFFF as mach_port_name_t) /* MACH_PORT_DEAD */, + "Receive rights cannot be dead names" + ) + _machPrecondition( + mach_port_destruct(mach_task_self_, _name, 0, _context) + ) + } else { + assert( + RightType.self == SendRight.self || + RightType.self == SendOnceRight.self + ) + _machPrecondition(mach_port_deallocate(mach_task_self_, _name)) + } + } + } + + /// Possible errors that can be thrown by Mach.Port operations. + public enum PortRightError : Error { + /// Returned when an operation cannot be completed, because the Mach + /// port right has become a dead name. This is caused by deallocation of the + /// receive right on the other end. + case deadName + } + + /// The MachPortRight type used to manage a receive right. + @frozen + public struct ReceiveRight: MachPortRight {} + + /// The MachPortRight type used to manage a send right. + @frozen + public struct SendRight: MachPortRight {} + + /// The MachPortRight type used to manage a send-once right. + /// + /// Send-once rights are the most restrictive type of Mach port rights. + /// They cannot create other rights, and are consumed upon use. + /// + /// Upon destruction a send-once notification will be sent to the + /// receiving end. + @frozen + public struct SendOnceRight: MachPortRight {} +} + +@available(System 1.4.0, *) +extension Mach.Port where RightType == Mach.ReceiveRight { + /// Transfer ownership of an existing, unmanaged, but already guarded, + /// Mach port right into a Mach.Port by name. + /// + /// This initializer aborts if name is MACH_PORT_NULL. + /// + /// If the type of the right does not match the type T of Mach.Port + /// being constructed, the behavior is undefined. + /// + /// The underlying port right will be automatically deallocated when + /// the Mach.Port object is destroyed. + public init(name: mach_port_name_t, context: mach_port_context_t) { + precondition(name != mach_port_name_t(MACH_PORT_NULL), + "Mach.Port cannot be initialized with MACH_PORT_NULL") + self._name = name + self._context = context + } + + /// Allocate a new Mach port with a receive right, creating a + /// Mach.Port to manage it. + /// + /// This initializer will abort if the right could not be created. + /// Callers may assert that a valid right is always returned. + @inlinable + @available(System 1.4.0, *) + public init() { + var storage: mach_port_name_t = mach_port_name_t(MACH_PORT_NULL) + _machPrecondition( + mach_port_allocate(mach_task_self_, MACH_PORT_RIGHT_RECEIVE, &storage) + ) + + // name-only init will guard ReceiveRights + self.init(name: storage) + } + + + /// Transfer ownership of the underlying port right to the caller. + /// + /// Returns a tuple containing the Mach port name representing the right, + /// and the context value used to guard the right. + /// + /// This operation liberates the right from management by the Mach.Port, + /// and the underlying right will no longer be automatically deallocated. + /// + /// After this function completes, the Mach.Port is destroyed and no longer + /// usable. + @inlinable + @available(System 1.4.0, *) + public consuming func relinquish( + ) -> (name: mach_port_name_t, context: mach_port_context_t) { + let destructured = (name: _name, context: _context) + discard self + return destructured + } + + /// Remove guard and transfer ownership of the underlying port right to + /// the caller. + /// + /// Returns the Mach port name representing the right. + /// + /// This operation liberates the right from management by the Mach.Port, + /// and the underlying right will no longer be automatically deallocated. + /// + /// After this function completes, the Mach.Port is destroyed and no longer + /// usable. + /// + /// This function makes a syscall to remove the guard from + /// Mach.ReceiveRights. Use relinquish() to avoid the syscall and extract + /// the context value along with the port name. + @inlinable + @available(System 1.4.0, *) + public consuming func unguardAndRelinquish() -> mach_port_name_t { + let (name, context) = self.relinquish() + _machPrecondition(mach_port_unguard(mach_task_self_, name, context)) + return name + } + + /// Borrow access to the port name in a block that can perform + /// non-consuming operations. + /// + /// Take care when using this function; many operations consume rights. + /// + /// If the right is consumed, behavior is undefined. + /// + /// The body block may optionally return something, which will then be + /// returned to the caller of withBorrowedName. + @inlinable + @available(System 1.4.0, *) + public func withBorrowedName( + body: (mach_port_name_t, mach_port_context_t) -> ReturnType + ) -> ReturnType { + return body(_name, _context) + } + + /// Create a send-once right for a given receive right. + /// + /// This does not affect the makeSendCount of the receive right. + /// + /// This function will abort if the right could not be created. + /// Callers may assert that a valid right is always returned. + @inlinable + @available(System 1.4.0, *) + public func makeSendOnceRight() -> Mach.Port { + // send once rights do not coalesce + var newRight: mach_port_name_t = mach_port_name_t(MACH_PORT_NULL) + var newRightType: mach_port_type_t = MACH_PORT_TYPE_NONE + + _machPrecondition( + mach_port_extract_right( + mach_task_self_, + _name, + mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND_ONCE), + &newRight, + &newRightType + ) + ) + + // The value of newRight is validated by the Mach.Port initializer + precondition(newRightType == MACH_MSG_TYPE_MOVE_SEND_ONCE) + + return Mach.Port(name: newRight) + } + + /// Create a send right for a given receive right. + /// + /// This increments the makeSendCount of the receive right. + /// + /// This function will abort if the right could not be created. + /// Callers may assert that a valid right is always returned. + @inlinable + @available(System 1.4.0, *) + public func makeSendRight() -> Mach.Port { + let how = MACH_MSG_TYPE_MAKE_SEND + + // name is the same because send and recv rights are coalesced + _machPrecondition( + mach_port_insert_right( + mach_task_self_, _name, _name, mach_msg_type_name_t(how) + ) + ) + + return Mach.Port(name: _name) + } + + /// Access the make-send count. + /// + /// Each get/set of this property makes a syscall. + @inlinable + @available(System 1.4.0, *) + public var makeSendCount: mach_port_mscount_t { + get { + var status: mach_port_status = mach_port_status() + var size = mach_msg_type_number_t( + MemoryLayout.size / MemoryLayout.size + ) + + withUnsafeMutablePointer(to: &status) { + [ _name = self._name ] in + let status = UnsafeMutableBufferPointer(start: $0, count: 1) + status.withMemoryRebound(to: integer_t.self) { + let info = $0.baseAddress + _machPrecondition( + mach_port_get_attributes( + mach_task_self_, _name, MACH_PORT_RECEIVE_STATUS, info, &size + ) + ) + } + } + return status.mps_mscount + } + + set { + _machPrecondition(mach_port_set_mscount(mach_task_self_, _name, newValue)) + } + } +} + +@available(System 1.4.0, *) +extension Mach.Port where RightType == Mach.SendRight { + /// Transfer ownership of the underlying port right to the caller. + /// + /// Returns the Mach port name representing the right. + /// + /// This operation liberates the right from management by the Mach.Port, + /// and the underlying right will no longer be automatically deallocated. + /// + /// After this function completes, the Mach.Port is destroyed and no longer + /// usable. + @inlinable + public consuming func relinquish() -> mach_port_name_t { + let name = _name + discard self + return name + } + + /// Create another send right from a given send right. + /// + /// This does not affect the makeSendCount of the receive right. + /// + /// If the send right being copied has become a dead name, meaning the + /// receiving side has been deallocated, then copySendRight() will throw + /// a Mach.PortRightError.deadName error. + @inlinable + @available(System 1.4.0, *) + public func copySendRight() throws -> Mach.Port { + let how = MACH_MSG_TYPE_COPY_SEND + + // name is the same because send rights are coalesced + let kr = mach_port_insert_right( + mach_task_self_, _name, _name, mach_msg_type_name_t(how) + ) + if kr == KERN_INVALID_NAME || kr == KERN_INVALID_CAPABILITY { + throw Mach.PortRightError.deadName + } + _machPrecondition(kr) + + return Mach.Port(name: _name) + } +} + +@available(System 1.4.0, *) +extension Mach.Port where RightType == Mach.SendOnceRight { + /// Transfer ownership of the underlying port right to the caller. + /// + /// Returns the Mach port name representing the right. + /// + /// This operation liberates the right from management by the Mach.Port, + /// and the underlying right will no longer be automatically deallocated. + /// + /// After this function completes, the Mach.Port is destroyed and no longer + /// usable. + @inlinable + @available(System 1.4.0, *) + public consuming func relinquish() -> mach_port_name_t { + let name = _name + discard self + return name + } +} + +#endif diff --git a/Sources/System/Platform.swift b/Sources/System/Platform.swift deleted file mode 100644 index 73e436dc..00000000 --- a/Sources/System/Platform.swift +++ /dev/null @@ -1,22 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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 -*/ - -@_implementationOnly import SystemInternals - -// Public typealiases that can't be reexported from SystemInternals - -/// The C `mode_t` type. -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -public typealias CModeT = UInt16 -#elseif os(Windows) -public typealias CModeT = Int32 -#else -public typealias CModeT = UInt32 -#endif diff --git a/Sources/System/PlatformString.swift b/Sources/System/PlatformString.swift new file mode 100644 index 00000000..7b8fd90c --- /dev/null +++ b/Sources/System/PlatformString.swift @@ -0,0 +1,197 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +@available(System 0.0.2, *) +extension String { + /// Creates a string by interpreting the null-terminated platform string as + /// UTF-8 on Unix and UTF-16 on Windows. + /// + /// - Parameter platformString: The null-terminated platform string to be + /// interpreted as `CInterop.PlatformUnicodeEncoding`. + /// + /// If the content of the platform string isn't well-formed Unicode, + /// this initializer replaces invalid bytes with U+FFFD. + /// This means that, depending on the semantics of the specific platform, + /// conversion to a string and back might result in a value that's different + /// from the original platform string. + @_disfavoredOverload + public init(platformString: UnsafePointer) { + self.init(_errorCorrectingPlatformString: platformString) + } + + /// Creates a string by interpreting the null-terminated platform string as + /// UTF-8 on Unix and UTF-16 on Windows. + /// + /// - Parameter platformString: The null-terminated platform string to be + /// interpreted as `CInterop.PlatformUnicodeEncoding`. + /// + /// - Note It is a precondition that `platformString` must be null-terminated. + /// The absence of a null byte will trigger a runtime error. + /// + /// If the content of the platform string isn't well-formed Unicode, + /// this initializer replaces invalid bytes with U+FFFD. + /// This means that, depending on the semantics of the specific platform, + /// conversion to a string and back might result in a value that's different + /// from the original platform string. + @inlinable + @_alwaysEmitIntoClient + public init(platformString: [CInterop.PlatformChar]) { + guard let _ = platformString.firstIndex(of: 0) else { + fatalError( + "input of String.init(platformString:) must be null-terminated" + ) + } + self = platformString.withUnsafeBufferPointer { + String(platformString: $0.baseAddress!) + } + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use String.init(_ scalar: Unicode.Scalar)") + public init(platformString: inout CInterop.PlatformChar) { + guard platformString == 0 else { + fatalError( + "input of String.init(platformString:) must be null-terminated" + ) + } + self = "" + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use a copy of the String argument") + public init(platformString: String) { + if let nullLoc = platformString.firstIndex(of: "\0") { + self = String(platformString[.. + ) { + self.init(_platformString: platformString) + } + + /// Creates a string by interpreting the null-terminated platform string as + /// UTF-8 on Unix and UTF-16 on Windows. + /// + /// - Parameter platformString: The null-terminated platform string to be + /// interpreted as `CInterop.PlatformUnicodeEncoding`. + /// + /// - Note It is a precondition that `platformString` must be null-terminated. + /// The absence of a null byte will trigger a runtime error. + /// + /// If the contents of the platform string isn't well-formed Unicode, + /// this initializer returns `nil`. + @inlinable + @_alwaysEmitIntoClient + public init?( + validatingPlatformString platformString: [CInterop.PlatformChar] + ) { + guard let _ = platformString.firstIndex(of: 0) else { + fatalError( + "input of String.init(validatingPlatformString:) must be null-terminated" + ) + } + guard let string = platformString.withUnsafeBufferPointer({ + String(validatingPlatformString: $0.baseAddress!) + }) else { + return nil + } + self = string + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use String(_ scalar: Unicode.Scalar)") + public init?( + validatingPlatformString platformString: inout CInterop.PlatformChar + ) { + guard platformString == 0 else { + fatalError( + "input of String.init(validatingPlatformString:) must be null-terminated" + ) + } + self = "" + } + + @inlinable + @_alwaysEmitIntoClient + @available(*, deprecated, message: "Use a copy of the String argument") + public init?( + validatingPlatformString platformString: String + ) { + if let nullLoc = platformString.firstIndex(of: "\0") { + self = String(platformString[..( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + try _withPlatformString(body) + } + +} + +@available(System 0.0.2, *) +extension CInterop.PlatformChar { + internal var _platformCodeUnit: CInterop.PlatformUnicodeEncoding.CodeUnit { + #if os(Windows) + return self + #else + return CInterop.PlatformUnicodeEncoding.CodeUnit(bitPattern: self) + #endif + } +} + +@available(System 0.0.2, *) +extension CInterop.PlatformUnicodeEncoding.CodeUnit { + internal var _platformChar: CInterop.PlatformChar { + #if os(Windows) + return self + #else + return CInterop.PlatformChar(bitPattern: self) + #endif + } +} + +internal protocol _PlatformStringable { + func _withPlatformString( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result + + init?(_platformString: UnsafePointer) +} +extension String: _PlatformStringable {} diff --git a/Sources/System/SystemString.swift b/Sources/System/SystemString.swift new file mode 100644 index 00000000..57abc625 --- /dev/null +++ b/Sources/System/SystemString.swift @@ -0,0 +1,331 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2024 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 +*/ + +// A platform-native character representation, currently used for file paths +internal struct SystemChar: + RawRepresentable, Sendable, Comparable, Hashable, Codable { + internal typealias RawValue = CInterop.PlatformChar + + internal var rawValue: RawValue + + internal init(rawValue: RawValue) { self.rawValue = rawValue } + + internal init(_ rawValue: RawValue) { self.init(rawValue: rawValue) } + + static func < (lhs: SystemChar, rhs: SystemChar) -> Bool { + lhs.rawValue < rhs.rawValue + } +} + +extension SystemChar { + internal init(ascii: Unicode.Scalar) { + self.init(rawValue: numericCast(UInt8(ascii: ascii))) + } + internal init(codeUnit: CInterop.PlatformUnicodeEncoding.CodeUnit) { + self.init(rawValue: codeUnit._platformChar) + } + + internal static var null: SystemChar { SystemChar(0x0) } + internal static var slash: SystemChar { SystemChar(ascii: "/") } + internal static var backslash: SystemChar { SystemChar(ascii: #"\"#) } + internal static var dot: SystemChar { SystemChar(ascii: ".") } + internal static var colon: SystemChar { SystemChar(ascii: ":") } + internal static var question: SystemChar { SystemChar(ascii: "?") } + + internal var codeUnit: CInterop.PlatformUnicodeEncoding.CodeUnit { + rawValue._platformCodeUnit + } + + internal var asciiScalar: Unicode.Scalar? { + guard isASCII else { return nil } + return Unicode.Scalar(UInt8(truncatingIfNeeded: rawValue)) + } + + internal var isASCII: Bool { + (0...0x7F).contains(rawValue) + } + + internal var isLetter: Bool { + guard isASCII else { return false } + let asciiRaw: UInt8 = numericCast(rawValue) + return (UInt8(ascii: "a") ... UInt8(ascii: "z")).contains(asciiRaw) || + (UInt8(ascii: "A") ... UInt8(ascii: "Z")).contains(asciiRaw) + } +} + +// A platform-native string representation, currently for file paths +// +// Always null-terminated. +internal struct SystemString: Sendable { + internal typealias Storage = [SystemChar] + internal var nullTerminatedStorage: Storage +} + +extension SystemString { + internal init() { + self.nullTerminatedStorage = [.null] + _invariantCheck() + } + + internal var length: Int { + let len = nullTerminatedStorage.count - 1 + assert(len == self.count) + return len + } + + // Common funnel point. Ensure all non-empty inits go here. + internal init(nullTerminated storage: Storage) { + self.nullTerminatedStorage = storage + _invariantCheck() + } + + // Ensures that result is null-terminated + internal init(_ chars: C) where C.Element == SystemChar { + var rawChars = Storage(chars) + if rawChars.last != .null { + rawChars.append(.null) + } + self.init(nullTerminated: rawChars) + } +} + +extension SystemString { + fileprivate func _invariantsSatisfied() -> Bool { + guard !nullTerminatedStorage.isEmpty else { return false } + guard nullTerminatedStorage.last! == .null else { return false } + guard nullTerminatedStorage.firstIndex(of: .null) == length else { + return false + } + return true + } + + fileprivate func _invariantCheck() { + #if DEBUG + precondition(_invariantsSatisfied()) + #endif // DEBUG + } +} + +extension SystemString: RandomAccessCollection, MutableCollection { + internal typealias Element = SystemChar + internal typealias Index = Storage.Index + internal typealias Indices = Range + + internal var startIndex: Index { + nullTerminatedStorage.startIndex + } + + internal var endIndex: Index { + nullTerminatedStorage.index(before: nullTerminatedStorage.endIndex) + } + + internal subscript(position: Index) -> SystemChar { + _read { + precondition(position >= startIndex && position <= endIndex) + yield nullTerminatedStorage[position] + } + set(newValue) { + precondition(position >= startIndex && position <= endIndex) + nullTerminatedStorage[position] = newValue + _invariantCheck() + } + } +} +extension SystemString: RangeReplaceableCollection { + internal mutating func replaceSubrange( + _ subrange: Range, with newElements: C + ) where C.Element == SystemChar { + defer { _invariantCheck() } + nullTerminatedStorage.replaceSubrange(subrange, with: newElements) + } + + internal mutating func reserveCapacity(_ n: Int) { + defer { _invariantCheck() } + nullTerminatedStorage.reserveCapacity(1 + n) + } + + internal func withContiguousStorageIfAvailable( + _ body: (UnsafeBufferPointer) throws -> R + ) rethrows -> R? { + // Do not include the null terminator, it is outside the Collection + try nullTerminatedStorage.withContiguousStorageIfAvailable { + try body(.init(start: $0.baseAddress, count: $0.count-1)) + } + } + + internal mutating func withContiguousMutableStorageIfAvailable( + _ body: (inout UnsafeMutableBufferPointer) throws -> R + ) rethrows -> R? { + defer { _invariantCheck() } + // Do not include the null terminator, it is outside the Collection + return try nullTerminatedStorage.withContiguousMutableStorageIfAvailable { + var buffer = UnsafeMutableBufferPointer( + start: $0.baseAddress, count: $0.count-1 + ) + return try body(&buffer) + } + } +} + +extension SystemString: Hashable, Codable { + // Encoder is synthesized; it probably should have been explicit and used + // a single-value container, but making that change now is somewhat risky. + + // Decoder is written explicitly to ensure that we validate invariants on + // untrusted input. + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.nullTerminatedStorage = try container.decode( + Storage.self, forKey: .nullTerminatedStorage + ) + guard _invariantsSatisfied() else { + throw DecodingError.dataCorruptedError( + forKey: .nullTerminatedStorage, + in: container, + debugDescription: + "Encoding does not satisfy the invariants of SystemString" + ) + } + } +} + +extension SystemString { + + internal func withNullTerminatedSystemChars( + _ f: (UnsafeBufferPointer) throws -> T + ) rethrows -> T { + try nullTerminatedStorage.withUnsafeBufferPointer(f) + } + + // withCodeUnits does not include the null terminator + internal func withCodeUnits( + _ f: (UnsafeBufferPointer) throws -> T + ) rethrows -> T { + try withNullTerminatedSystemChars { + try $0.withMemoryRebound(to: CInterop.PlatformUnicodeEncoding.CodeUnit.self) { + assert($0.last == .zero) + return try f(.init(start: $0.baseAddress, count: $0.count&-1)) + } + } + } +} + +extension Slice where Base == SystemString { + internal func withCodeUnits( + _ f: (UnsafeBufferPointer) throws -> T + ) rethrows -> T { + try base.withCodeUnits { + try f(UnsafeBufferPointer(rebasing: $0[indices])) + } + } + + internal var string: String { + withCodeUnits { + String(decoding: $0, as: CInterop.PlatformUnicodeEncoding.self) + } + } + + internal func withPlatformString( + _ f: (UnsafePointer) throws -> T + ) rethrows -> T { + // FIXME: avoid allocation if we're at the end + return try SystemString(self).withPlatformString(f) + } + +} + +extension String { + internal init(decoding str: SystemString) { + // TODO: Can avoid extra strlen + self = str.withPlatformString { + String(platformString: $0) + } + } + internal init?(validating str: SystemString) { + // TODO: Can avoid extra strlen + guard let str = str.withPlatformString(String.init(validatingPlatformString:)) + else { return nil } + + self = str + } +} + +extension SystemString: ExpressibleByStringLiteral { + internal init(stringLiteral: String) { + self.init(stringLiteral) + } + + internal init(_ string: String) { + // TODO: can avoid extra strlen + self = string.withPlatformString { + SystemString(platformString: $0) + } + } +} + +extension SystemString: CustomStringConvertible, CustomDebugStringConvertible { + internal var string: String { + self.withCodeUnits { + String(decoding: $0, as: CInterop.PlatformUnicodeEncoding.self) + } + } + + internal var description: String { string } + internal var debugDescription: String { description.debugDescription } +} + +extension SystemString { + /// Creates a system string by copying bytes from a null-terminated platform string. + /// + /// - Parameter platformString: A pointer to a null-terminated platform string. + internal init(platformString: UnsafePointer) { + let count = 1 + system_platform_strlen(platformString) + + // TODO: Is this the right way? + let chars: Array = platformString.withMemoryRebound( + to: SystemChar.self, capacity: count + ) { + let bufPtr = UnsafeBufferPointer(start: $0, count: count) + return Array(bufPtr) + } + + self.init(nullTerminated: chars) + } + + /// Calls the given closure with a pointer to the contents of the sytem string, + /// represented as a null-terminated platform string. + /// + /// - Parameter body: A closure with a pointer parameter + /// that points to a null-terminated platform string. + /// If `body` has a return value, + /// that value is also used as the return value for this method. + /// - Returns: The return value, if any, of the `body` closure parameter. + /// + /// The pointer passed as an argument to `body` is valid + /// only during the execution of this method. + /// Don't try to store the pointer for later use. + internal func withPlatformString( + _ f: (UnsafePointer) throws -> T + ) rethrows -> T { + try withNullTerminatedSystemChars { chars in + let length = chars.count * MemoryLayout.stride + return try chars.baseAddress!.withMemoryRebound( + to: CInterop.PlatformChar.self, + capacity: length / MemoryLayout.stride + ) { pointer in + assert(pointer[self.count] == 0) + return try f(pointer) + } + } + } +} + +// TODO: SystemString should use a COW-interchangable storage form rather +// than array, so you could "borrow" the storage from a non-bridged String +// or Data or whatever diff --git a/Sources/System/Util+StringArray.swift b/Sources/System/Util+StringArray.swift new file mode 100644 index 00000000..2a8ba286 --- /dev/null +++ b/Sources/System/Util+StringArray.swift @@ -0,0 +1,117 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 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 +*/ + +extension Array where Element == String { + internal typealias CStr = UnsafePointer? + + /// Call `body` with a buffer of `UnsafePointer?` values, + /// suitable for passing to a C function that expects a C string array. + /// The buffer is guaranteed to be followed by an extra storage slot + /// containing a null pointer. (For C functions that expect an array + /// terminated with a null pointer.) + /// + /// This function is careful not to heap allocate memory unless there are + /// too many strings, or if it needs to copy too much character data. + internal func _withCStringArray( + _ body: (UnsafeBufferPointer?>) throws -> R + ) rethrows -> R { + if self.count == 0 { + // Fast path: empty array. + let p: CStr = nil + return try Swift.withUnsafePointer(to: p) { array in + try body(UnsafeBufferPointer(start: array, count: 0)) + } + } + #if SYSTEM_OS_BUILD // String._guts isn't accessible from SwiftPM or CMake + if self.count == 1, self[0]._guts._isLargeZeroTerminatedContiguousUTF8 { + // Fast path: Single fast string. + let start = self[0]._guts._largeContiguousUTF8CodeUnits.baseAddress! + var p: (CStr, CStr) = ( + UnsafeRawPointer(start).assumingMemoryBound(to: CChar.self), + nil + ) + return try Swift.withUnsafeBytes(of: &p) { buffer in + let start = buffer.baseAddress!.assumingMemoryBound(to: CStr.self) + return try body(UnsafeBufferPointer(start: start, count: 1)) + } + } + #endif + // We need to create a buffer for the C array. + return try _withStackBuffer( + capacity: (self.count + 1) * MemoryLayout.stride + ) { array in + let array = array.bindMemory(to: CStr.self) + // Calculate number of bytes we need for character storage + let bytes = self.reduce(into: 0) { count, string in + #if SYSTEM_OS_BUILD + if string._guts._isLargeZeroTerminatedContiguousUTF8 { return } + #endif + count += string.utf8.count + 1 // Plus one for terminating NUL + } + #if SYSTEM_OS_BUILD + if bytes == 0 { + // Fast path: we only contain strings with stable null-terminated storage + for i in self.indices { + let string = self[i] + precondition(string._guts._isLargeZeroTerminatedContiguousUTF8) + let address = string._guts._largeContiguousUTF8CodeUnits.baseAddress! + array[i] = UnsafeRawPointer(address).assumingMemoryBound(to: CChar.self) + } + array[self.count] = nil + return try body(UnsafeBufferPointer(rebasing: array.dropLast())) + } + #endif + return try _withStackBuffer(capacity: bytes) { chars in + var chars = chars + for i in self.indices { + let (cstr, scratchUsed) = self[i]._getCStr(with: chars) + array[i] = cstr.assumingMemoryBound(to: CChar.self) + chars = .init(rebasing: chars[scratchUsed...]) + } + array[self.count] = nil + return try body(UnsafeBufferPointer(rebasing: array.dropLast())) + } + } + } +} + +extension String { + fileprivate func _getCStr( + with scratch: UnsafeMutableRawBufferPointer + ) -> (cstr: UnsafeRawPointer, scratchUsed: Int) { + #if SYSTEM_OS_BUILD + if _guts._isLargeZeroTerminatedContiguousUTF8 { + // This is a wonderful string, we can just use its storage address. + let address = _guts._largeContiguousUTF8CodeUnits.baseAddress! + return (UnsafeRawPointer(address), 0) + } + #endif + let r: (UnsafeRawPointer, Int)? = self.utf8.withContiguousStorageIfAvailable { source in + // This is a somewhat okay string -- we need to use memcpy. + precondition(source.count <= scratch.count) + let start = scratch.baseAddress! + start.copyMemory(from: source.baseAddress!, byteCount: source.count) + start.storeBytes(of: 0, toByteOffset: source.count, as: UInt8.self) + return (UnsafeRawPointer(start), source.count + 1) + } + if let r = r { return r } + + // What a horrible string; we need to copy individual bytes. + precondition(self.utf8.count <= scratch.count) + var c = 0 + for byte in self.utf8 { + scratch[c] = byte + c += 1 + } + scratch[c] = 0 + c += 1 + return (UnsafeRawPointer(scratch.baseAddress!), c) + } +} + diff --git a/Sources/System/Util.swift b/Sources/System/Util.swift index 2d966784..e4832ac5 100644 --- a/Sources/System/Util.swift +++ b/Sources/System/Util.swift @@ -1,28 +1,28 @@ /* 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 - 2021 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 */ // Results in errno if i == -1 -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -internal func valueOrErrno( +@available(System 0.0.1, *) +private func valueOrErrno( _ i: I ) -> Result { i == -1 ? .failure(Errno.current) : .success(i) } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -internal func nothingOrErrno( +@available(System 0.0.1, *) +private func nothingOrErrno( _ i: I ) -> Result<(), Errno> { valueOrErrno(i).map { _ in () } } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) internal func valueOrErrno( retryOnInterrupt: Bool, _ f: () -> I ) -> Result { @@ -36,17 +36,30 @@ internal func valueOrErrno( } while true } +@available(System 0.0.1, *) +internal func nothingOrErrno( + retryOnInterrupt: Bool, _ f: () -> I +) -> Result<(), Errno> { + valueOrErrno(retryOnInterrupt: retryOnInterrupt, f).map { _ in () } +} + // Run a precondition for debug client builds internal func _debugPrecondition( - _ condition: @autoclosure () -> Bool, _ message: StaticString = StaticString(), + _ condition: @autoclosure () -> Bool, + _ message: StaticString = StaticString(), file: StaticString = #file, line: UInt = #line ) { // Only check in debug mode. - if _slowPath(_isDebugAssertConfiguration()) { precondition(condition()) } + if _slowPath(_isDebugAssertConfiguration()) { + precondition( + condition(), String(describing: message), file: file, line: line) + } } extension OpaquePointer { - internal var _isNULL: Bool { OpaquePointer(bitPattern: Int(bitPattern: self)) == nil } + internal var _isNULL: Bool { + OpaquePointer(bitPattern: Int(bitPattern: self)) == nil + } } extension Sequence { @@ -94,3 +107,58 @@ extension OptionSet { return result } } + +internal func _dropCommonPrefix( + _ lhs: C, _ rhs: C +) -> (C.SubSequence, C.SubSequence) +where C.Element: Equatable { + var (lhs, rhs) = (lhs[...], rhs[...]) + while lhs.first != nil && lhs.first == rhs.first { + lhs.removeFirst() + rhs.removeFirst() + } + return (lhs, rhs) +} + +extension MutableCollection where Element: Equatable { + mutating func _replaceAll(_ e: Element, with new: Element) { + for idx in self.indices { + if self[idx] == e { self[idx] = new } + } + } +} + +internal func _withOptionalUnsafePointerOrNull( + to value: T?, + _ body: (UnsafePointer?) throws -> R +) rethrows -> R { + guard let value = value else { + return try body(nil) + } + return try withUnsafePointer(to: value, body) +} + +/// Calls `body` with a temporary buffer of the indicated size, +/// possibly stack-allocated. +internal func _withStackBuffer( + capacity: Int, + _ body: (UnsafeMutableRawBufferPointer) throws -> R +) rethrows -> R { + typealias StackStorage = ( + UInt64, UInt64, UInt64, UInt64, + UInt64, UInt64, UInt64, UInt64, + UInt64, UInt64, UInt64, UInt64, + UInt64, UInt64, UInt64, UInt64 + ) + if capacity > MemoryLayout.size { + var buffer = _RawBuffer(minimumCapacity: capacity) + return try buffer.withUnsafeMutableBytes { buffer in + try body(.init(rebasing: buffer[.. Bool) -> Element? { + guard let s = self.first, p(s) else { return nil } + self = self.dropFirst() + return s + } + internal mutating func _eat(_ e: Element) -> Element? { + _eat(if: { $0 == e }) + } + + internal mutating func _eat(asserting e: Element) { + let p = _eat(e) + assert(p != nil) + } + + internal mutating func _eat(count c: Int) -> Slice { + defer { self = self.dropFirst(c) } + return self.prefix(c) + } + + internal mutating func _eatSequence(_ es: C) -> Slice? + where C.Element == Element + { + guard self.starts(with: es) else { return nil } + return _eat(count: es.count) + } + + internal mutating func _eatUntil(_ idx: Index) -> Slice { + precondition(idx >= startIndex && idx <= endIndex) + defer { self = self[idx...] } + return self[.. Slice { + precondition(idx >= startIndex && idx <= endIndex) + guard idx != endIndex else { + defer { self = self[endIndex ..< endIndex] } + return self + } + defer { self = self[index(after: idx)...] } + return self[...idx] + } + + // If `e` is present, eat up to first occurrence of `e` + internal mutating func _eatUntil(_ e: Element) -> Slice? { + guard let idx = self.firstIndex(of: e) else { return nil } + return _eatUntil(idx) + } + + // If `e` is present, eat up to and through first occurrence of `e` + internal mutating func _eatThrough(_ e: Element) -> Slice? { + guard let idx = self.firstIndex(of: e) else { return nil } + return _eatThrough(idx) + } + + // Eat any elements from the front matching the predicate + internal mutating func _eatWhile(_ p: (Element) -> Bool) -> Slice? { + let idx = firstIndex(where: { !p($0) }) ?? endIndex + guard idx != startIndex else { return nil } + return _eatUntil(idx) + } +} diff --git a/Sources/System/WindowsPlatformConstants.swift b/Sources/System/WindowsPlatformConstants.swift deleted file mode 100644 index 7b26eda2..00000000 --- a/Sources/System/WindowsPlatformConstants.swift +++ /dev/null @@ -1,328 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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) - -@_alwaysEmitIntoClient -internal var _EPERM: CInt { 1 } - -@_alwaysEmitIntoClient -internal var _ENOENT: CInt { 2 } - -@_alwaysEmitIntoClient -internal var _ESRCH: CInt { 3 } - -@_alwaysEmitIntoClient -internal var _EINTR: CInt { 4 } - -@_alwaysEmitIntoClient -internal var _EIO: CInt { 5 } - -@_alwaysEmitIntoClient -internal var _ENXIO: CInt { 6 } - -@_alwaysEmitIntoClient -internal var _E2BIG: CInt { 7 } - -@_alwaysEmitIntoClient -internal var _ENOEXEC: CInt { 8 } - -@_alwaysEmitIntoClient -internal var _EBADF: CInt { 9 } - -@_alwaysEmitIntoClient -internal var _ECHILD: CInt { 10 } - -@_alwaysEmitIntoClient -internal var _EAGAIN: CInt { 11 } - -@_alwaysEmitIntoClient -internal var _ENOMEM: CInt { 12 } - -@_alwaysEmitIntoClient -internal var _EACCES: CInt { 13 } - -@_alwaysEmitIntoClient -internal var _EFAULT: CInt { 14 } - -@_alwaysEmitIntoClient -internal var _EBUSY: CInt { 16 } - -@_alwaysEmitIntoClient -internal var _EEXIST: CInt { 17 } - -@_alwaysEmitIntoClient -internal var _EXDEV: CInt { 18 } - -@_alwaysEmitIntoClient -internal var _ENODEV: CInt { 19 } - -@_alwaysEmitIntoClient -internal var _ENOTDIR: CInt { 20 } - -@_alwaysEmitIntoClient -internal var _EISDIR: CInt { 21 } - -@_alwaysEmitIntoClient -internal var _EINVAL: CInt { 22 } - -@_alwaysEmitIntoClient -internal var _ENFILE: CInt { 23 } - -@_alwaysEmitIntoClient -internal var _EMFILE: CInt { 24 } - -@_alwaysEmitIntoClient -internal var _ENOTIFY: CInt { 25 } - -@_alwaysEmitIntoClient -internal var _EFBIG: CInt { 27 } - -@_alwaysEmitIntoClient -internal var _ENOSPC: CInt { 28 } - -@_alwaysEmitIntoClient -internal var _ESPIPE: CInt { 29 } - -@_alwaysEmitIntoClient -internal var _EROFS: CInt { 30 } - -@_alwaysEmitIntoClient -internal var _EMLINK: CInt { 31 } - -@_alwaysEmitIntoClient -internal var _EPIPE: CInt { 32 } - -@_alwaysEmitIntoClient -internal var _EDOM: CInt { 33 } - -@_alwaysEmitIntoClient -internal var _ERANGE: CInt { 34 } - -@_alwaysEmitIntoClient -internal var _EDEADLK: CInt { 36 } - -@_alwaysEmitIntoClient -internal var _EDEADLOCK: CInt { 36 } - -@_alwaysEmitIntoClient -internal var _ENAMETOOLONG: CInt { 38 } - -@_alwaysEmitIntoClient -internal var _ENOLCK: CInt { 39 } - -@_alwaysEmitIntoClient -internal var _ENOSYS: CInt { 40 } - -@_alwaysEmitIntoClient -internal var _ENOTEMPTY: CInt { 41 } - -@_alwaysEmitIntoClient -internal var _EILSEQ: CInt { 42 } - -@_alwaysEmitIntoClient -internal var _STRUNCATE: CInt { 80 } - - -@_alwaysEmitIntoClient -internal var _O_RDONLY: CInt { 0x0000 } - -@_alwaysEmitIntoClient -internal var _O_WRONLY: CInt { 0x0001 } - -@_alwaysEmitIntoClient -internal var _O_RDWR: CInt { 0x0002 } - -@_alwaysEmitIntoClient -internal var _O_APPEND: CInt { 0x0008 } - -@_alwaysEmitIntoClient -internal var _O_CREAT: CInt { 0x0100 } - -@_alwaysEmitIntoClient -internal var _O_TRUNC: CInt { 0x0200 } - -@_alwaysEmitIntoClient -internal var _O_EXCL: CInt { 0x0400 } - - -@_alwaysEmitIntoClient -internal var _SEEK_SET: CInt { 0 } - -@_alwaysEmitIntoClient -internal var _SEEK_CUR: CInt { 1 } - -@_alwaysEmitIntoClient -internal var _SEEK_END: CInt { 2 } - - -// WinSock2 - -@_alwaysEmitIntoClient -internal var __EINTR: CInt { 10004 } - -@_alwaysEmitIntoClient -internal var __EBADF: CInt { 10009 } - -@_alwaysEmitIntoClient -internal var __EACCES: CInt { 10013 } - -@_alwaysEmitIntoClient -internal var __EFAULT: CInt { 10014 } - -@_alwaysEmitIntoClient -internal var __EINVAL: CInt { 10022 } - -@_alwaysEmitIntoClient -internal var __EMFILE: CInt { 10024 } - - -@_alwaysEmitIntoClient -internal var _EWOULDBLOCK: CInt { 10035 } - -@_alwaysEmitIntoClient -internal var _EINPROGRESS: CInt { 10036 } - -@_alwaysEmitIntoClient -internal var _EALREADY: CInt { 10037 } - -@_alwaysEmitIntoClient -internal var _ENOTSOCK: CInt { 10038 } - -@_alwaysEmitIntoClient -internal var _EDESTADDRREQ: CInt { 10039 } - -@_alwaysEmitIntoClient -internal var _EMSGSIZE: CInt { 10040 } - -@_alwaysEmitIntoClient -internal var _EPROTOTYPE: CInt { 10041 } - -@_alwaysEmitIntoClient -internal var _ENOPROTOOPT: CInt { 10042 } - -@_alwaysEmitIntoClient -internal var _EPROTONOSUPPORT: CInt { 10043 } - -@_alwaysEmitIntoClient -internal var _ESOCKTNOSUPPORT: CInt { 10044 } - -@_alwaysEmitIntoClient -internal var _EOPNOTSUPP: CInt { 10045 } - -@_alwaysEmitIntoClient -internal var _EPFNOSUPPORT: CInt { 10046 } - -@_alwaysEmitIntoClient -internal var _EAFNOSUPPORT: CInt { 10047 } - -@_alwaysEmitIntoClient -internal var _EADDRINUSE: CInt { 10048 } - -@_alwaysEmitIntoClient -internal var _EADDRNOTAVAIL: CInt { 10049 } - -@_alwaysEmitIntoClient -internal var _ENETDOWN: CInt { 10050 } - -@_alwaysEmitIntoClient -internal var _ENETUNREACH: CInt { 10051 } - -@_alwaysEmitIntoClient -internal var _ENETRESET: CInt { 10052 } - -@_alwaysEmitIntoClient -internal var _ECONNABORTED: CInt { 10053 } - -@_alwaysEmitIntoClient -internal var _ECONNRESET: CInt { 10054 } - -@_alwaysEmitIntoClient -internal var _ENOBUFS: CInt { 10055 } - -@_alwaysEmitIntoClient -internal var _EISCONN: CInt { 10056 } - -@_alwaysEmitIntoClient -internal var _ENOTCONN: CInt { 10057 } - -@_alwaysEmitIntoClient -internal var _ESHUTDOWN: CInt { 10058 } - -@_alwaysEmitIntoClient -internal var _ETOOMANYREFS: CInt { 10059 } - -@_alwaysEmitIntoClient -internal var _ETIMEDOUT: CInt { 10060 } - -@_alwaysEmitIntoClient -internal var _ECONNREFUSED: CInt { 10061 } - -@_alwaysEmitIntoClient -internal var _ELOOP: CInt { 10062 } - -@_alwaysEmitIntoClient -internal var __ENAMETOOLONG: CInt { 10063 } - -@_alwaysEmitIntoClient -internal var _EHOSTDOWN: CInt { 10064 } - -@_alwaysEmitIntoClient -internal var _EHOSTUNREACH: CInt { 10065 } - -@_alwaysEmitIntoClient -internal var __ENOTEMPTY: CInt { 10066 } - -@_alwaysEmitIntoClient -internal var _EPROCLIM: CInt { 10067 } - -@_alwaysEmitIntoClient -internal var _EUSERS: CInt { 10068 } - -@_alwaysEmitIntoClient -internal var _EDQUOT: CInt { 10069 } - -@_alwaysEmitIntoClient -internal var _ESTALE: CInt { 10070 } - -@_alwaysEmitIntoClient -internal var _EREMOTE: CInt { 10071 } - -// WSASYSNOTREADY = 10091 -// WSAVERNOTSUPPORTED = 10092 -// WSANOTINITIALIZED = 10093 - -@_alwaysEmitIntoClient -internal var _EDISCON: CInt { 10101 } - -@_alwaysEmitIntoClient -internal var _ENOMORE: CInt { 10102 } - -@_alwaysEmitIntoClient -internal var _ECANCELED: CInt { 10103 } - -@_alwaysEmitIntoClient -internal var _EINVALIDPROCTABLE: CInt { 10104 } - -@_alwaysEmitIntoClient -internal var _EINVALIDPROVIDER: CInt { 10105 } - -@_alwaysEmitIntoClient -internal var _EPROVIDERFAILEDINIT: CInt { 10106 } - -// WSASYSCALLFAILURE = 10107 -// WSASERVICE_NOT_FOUND = 10108 -// WSATYPE_NOT_FOUND = 10109 -// WSA_E_NO_MORE = 10110 -// WSA_E_CANCELLED = 10111 - -@_alwaysEmitIntoClient -internal var _EREFUSED: CInt { 10112 } - -#endif diff --git a/Sources/SystemInternals/Exports.swift b/Sources/SystemInternals/Exports.swift deleted file mode 100644 index 6162b8b4..00000000 --- a/Sources/SystemInternals/Exports.swift +++ /dev/null @@ -1,67 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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 -*/ - -import CSystem - -// TODO: Should CSystem just include all the header files we need? - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -import Darwin -#elseif os(Linux) || os(FreeBSD) || os(Android) -import Glibc -#elseif os(Windows) -import ucrt -#else -#error("Unsupported Platform") -#endif - -public typealias COffT = off_t - -#if os(Windows) -public typealias CModeT = CInt -#else -public typealias CModeT = mode_t -#endif - -// MARK: syscalls and variables - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -public var system_errno: CInt { - get { Darwin.errno } - set { Darwin.errno = newValue } -} -#elseif os(Windows) -public var system_errno: CInt { - get { - var value: CInt = 0 - // TODO(compnerd) handle the error? - _ = ucrt._get_errno(&value) - return value - } - set { - _ = ucrt._set_errno(newValue) - } -} -#else -public var system_errno: CInt { - get { Glibc.errno } - set { Glibc.errno = newValue } -} -#endif - -// MARK: C stdlib decls - -public func system_strerror(_ __errnum: Int32) -> UnsafeMutablePointer! { - strerror(__errnum) -} - -public func system_strlen(_ s: UnsafePointer) -> Int { - strlen(s) -} - diff --git a/Sources/SystemInternals/Mocking.swift b/Sources/SystemInternals/Mocking.swift deleted file mode 100644 index aa4bdbb9..00000000 --- a/Sources/SystemInternals/Mocking.swift +++ /dev/null @@ -1,192 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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 -*/ - -// Syscall mocking support. -// -// NOTE: This is currently the bare minimum needed for System's testing purposes, though we do -// eventually want to expose some solution to users. -// -// Mocking is contextual, accessible through MockingDriver.withMockingEnabled. Mocking -// state, including whether it is enabled, is stored in thread-local storage. Mocking is only -// enabled in testing builds of System currently, to minimize runtime overhead of release builds. -// - -#if ENABLE_MOCKING -public struct Trace { - public struct Entry: Hashable { - var name: String - var arguments: [AnyHashable] - - public init(name: String, _ arguments: [AnyHashable]) { - self.name = name - self.arguments = arguments - } - } - - private var entries: [Entry] = [] - private var firstEntry: Int = 0 - - public var isEmpty: Bool { firstEntry >= entries.count } - - public mutating func dequeue() -> Entry? { - guard !self.isEmpty else { return nil } - defer { firstEntry += 1 } - return entries[firstEntry] - } - - internal mutating func add(_ e: Entry) { - entries.append(e) - } - - public mutating func clear() { entries.removeAll() } -} - -// TODO: Track -public struct WriteBuffer { - public var enabled: Bool = false - - private var buffer: [UInt8] = [] - private var chunkSize: Int? = nil - - internal mutating func write(_ buf: UnsafeRawBufferPointer) -> Int { - guard enabled else { return 0 } - let chunk = chunkSize ?? buf.count - buffer.append(contentsOf: buf.prefix(chunk)) - return chunk - } - - public var contents: [UInt8] { buffer } -} - -public enum ForceErrno: Equatable { - case none - case always(errno: CInt) - - case counted(errno: CInt, count: Int) -} - -// Provide access to the driver, context, and trace stack of mocking -public class MockingDriver { - // Record syscalls and their arguments - public var trace = Trace() - - // Mock errors inside syscalls - public var forceErrno = ForceErrno.none - - // A buffer to put `write` bytes into - public var writeBuffer = WriteBuffer() -} - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -import Darwin -#elseif os(Linux) || os(FreeBSD) || os(Android) -import Glibc -#elseif os(Windows) -import ucrt -import WinSDK -#else -#error("Unsupported Platform") -#endif - -// TLS helper functions -#if os(Windows) -internal typealias TLSKey = DWORD -internal func makeTLSKey() -> TLSKey { - let raw: DWORD = FlsAlloc(nil) - if raw == FLS_OUT_OF_INDEXES { - fatalError("Unable to create key") - } - return raw -} -internal func setTLS(_ key: TLSKey, _ p: UnsafeMutableRawPointer?) { - guard FlsSetValue(key, p) else { - fatalError("Unable to set TLS") - } -} -internal func getTLS(_ key: TLSKey) -> UnsafeMutableRawPointer? { - FlsGetValue(key) -} - -#else - -internal typealias TLSKey = pthread_key_t -internal func makeTLSKey() -> TLSKey { - var raw = pthread_key_t() - guard 0 == pthread_key_create(&raw, nil) else { - fatalError("Unable to create key") - } - return raw -} -internal func setTLS(_ key: TLSKey, _ p: UnsafeMutableRawPointer?) { - guard 0 == pthread_setspecific(key, p) else { - fatalError("Unable to set TLS") - } -} -internal func getTLS(_ key: TLSKey) -> UnsafeMutableRawPointer? { - pthread_getspecific(key) -} -#endif - -private let driverKey: TLSKey = { makeTLSKey() }() - -internal var currentMockingDriver: MockingDriver? { - #if !ENABLE_MOCKING - fatalError("Contextual mocking in non-mocking build") - #endif - - guard let rawPtr = getTLS(driverKey) else { return nil } - - return Unmanaged.fromOpaque(rawPtr).takeUnretainedValue() -} - -extension MockingDriver { - /// Enables mocking for the duration of `f` with a clean trace queue - /// Restores prior mocking status and trace queue after execution - public static func withMockingEnabled( - _ f: (MockingDriver) throws -> () - ) rethrows { - let priorMocking = currentMockingDriver - let driver = MockingDriver() - - defer { - if let object = priorMocking { - setTLS(driverKey, Unmanaged.passUnretained(object).toOpaque()) - } else { - setTLS(driverKey, nil) - } - _fixLifetime(driver) - } - - setTLS(driverKey, Unmanaged.passUnretained(driver).toOpaque()) - return try f(driver) - } -} - -// Check TLS for mocking -@inline(never) -private var contextualMockingEnabled: Bool { - return currentMockingDriver != nil -} - -extension MockingDriver { - public static var enabled: Bool { mockingEnabled } -} - -#endif // ENABLE_MOCKING - -@inline(__always) -internal var mockingEnabled: Bool { - // Fast constant-foldable check for release builds - #if ENABLE_MOCKING - return contextualMockingEnabled - #else - return false - #endif -} - diff --git a/Sources/SystemInternals/Syscalls.swift b/Sources/SystemInternals/Syscalls.swift deleted file mode 100644 index 694018c5..00000000 --- a/Sources/SystemInternals/Syscalls.swift +++ /dev/null @@ -1,148 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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(macOS) || os(iOS) || os(watchOS) || os(tvOS) -import Darwin -#elseif os(Linux) || os(FreeBSD) || os(Android) -import Glibc -#elseif os(Windows) -import ucrt -#else -#error("Unsupported Platform") -#endif - -#if ENABLE_MOCKING -// Strip the mock_system prefix and the arg list suffix -private func originalSyscallName(_ s: String) -> String { - precondition(s.starts(with: "system_")) - return String(s.dropFirst("system_".count).prefix { $0.isLetter }) -} - -private func mockImpl( - name: String, - _ args: [AnyHashable] -) -> CInt { - let origName = originalSyscallName(name) - guard let driver = currentMockingDriver else { - fatalError("Mocking requested from non-mocking context") - } - driver.trace.add(Trace.Entry(name: origName, args)) - - switch driver.forceErrno { - case .none: break - case .always(let e): - system_errno = e - return -1 - case .counted(let e, let count): - assert(count >= 1) - system_errno = e - driver.forceErrno = count > 1 ? .counted(errno: e, count: count-1) : .none - return -1 - } - - return 0 -} - -private func mock( - name: String = #function, _ args: AnyHashable... -) -> CInt { - precondition(mockingEnabled) - return mockImpl(name: name, args) -} -private func mockInt( - name: String = #function, _ args: AnyHashable... -) -> Int { - Int(mockImpl(name: name, args)) -} - -private func mockOffT( - name: String = #function, _ args: AnyHashable... -) -> off_t { - off_t(mockImpl(name: name, args)) -} -#endif // ENABLE_MOCKING - -// Interacting with the mocking system, tracing, etc., is a potentially significant -// amount of code size, so we hand outline that code for every syscall - -// open -public func system_open(_ path: UnsafePointer, _ oflag: Int32) -> CInt { -#if ENABLE_MOCKING - if mockingEnabled { return mock(String(cString: path), oflag) } -#endif - return open(path, oflag) -} - -public func system_open( - _ path: UnsafePointer, _ oflag: Int32, _ mode: CModeT -) -> CInt { -#if ENABLE_MOCKING - if mockingEnabled { return mock(String(cString: path), oflag, mode) } -#endif - return open(path, oflag, mode) -} - -// close -public func system_close(_ fd: Int32) -> Int32 { -#if ENABLE_MOCKING - if mockingEnabled { return mock(fd) } -#endif - return close(fd) -} - -// read -public func system_read( - _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int -) -> Int { -#if ENABLE_MOCKING - if mockingEnabled { return mockInt(fd, buf, nbyte) } -#endif - return read(fd, buf, nbyte) -} - -// pread -public func system_pread( - _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int, _ offset: off_t -) -> Int { -#if ENABLE_MOCKING - if mockingEnabled { return mockInt(fd, buf, nbyte, offset) } -#endif - return pread(fd, buf, nbyte, offset) -} - -// lseek -public func system_lseek( - _ fd: Int32, _ off: off_t, _ whence: Int32 -) -> off_t { -#if ENABLE_MOCKING - if mockingEnabled { return mockOffT(fd, off, whence) } -#endif - return lseek(fd, off, whence) -} - -// write -public func system_write( - _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int -) -> Int { -#if ENABLE_MOCKING - if mockingEnabled { return mockInt(fd, buf, nbyte) } -#endif - return write(fd, buf, nbyte) -} - -// pwrite -public func system_pwrite( - _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int, _ offset: off_t -) -> Int { -#if ENABLE_MOCKING - if mockingEnabled { return mockInt(fd, buf, nbyte, offset) } -#endif - return pwrite(fd, buf, nbyte, offset) -} - diff --git a/Sources/SystemInternals/WindowsSyscallAdapters.swift b/Sources/SystemInternals/WindowsSyscallAdapters.swift deleted file mode 100644 index 2cc46d20..00000000 --- a/Sources/SystemInternals/WindowsSyscallAdapters.swift +++ /dev/null @@ -1,105 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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 ucrt -import WinSDK - -@inline(__always) -internal func open(_ path: UnsafePointer, _ oflag: Int32) -> CInt { - var fh: CInt = -1 - _ = _sopen_s(&fh, path, oflag, _SH_DENYNO, _S_IREAD | _S_IWRITE) - return fh -} - -@inline(__always) -internal func open( - _ path: UnsafePointer, _ oflag: Int32, _ mode: CModeT -) -> CInt { - // TODO(compnerd): Apply read/write permissions - var fh: CInt = -1 - _ = _sopen_s(&fh, path, oflag, _SH_DENYNO, _S_IREAD | _S_IWRITE) - return fh -} - -@inline(__always) -internal func close(_ fd: Int32) -> Int32 { - _close(fd) -} - -@inline(__always) -internal func lseek( - _ fd: Int32, _ off: Int64, _ whence: Int32 -) -> Int64 { - _lseeki64(fd, off, whence) -} - -@inline(__always) -internal func read( - _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int -) -> Int { - Int(_read(fd, buf, numericCast(nbyte))) -} - -@inline(__always) -internal func write( - _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int -) -> Int { - Int(_write(fd, buf, numericCast(nbyte))) -} - -@inline(__always) -internal func pread( - _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int, _ offset: off_t -) -> Int { - let handle: intptr_t = _get_osfhandle(fd) - if handle == /* INVALID_HANDLE_VALUE */ -1 { return Int(EBADF) } - - // NOTE: this is a non-owning handle, do *not* call CloseHandle on it - let hFile: HANDLE = HANDLE(bitPattern: handle)! - - var ovlOverlapped: OVERLAPPED = OVERLAPPED() - ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff) - ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff) - - var nNumberOfBytesRead: DWORD = 0 - if !ReadFile(hFile, buf, DWORD(nbyte), &nNumberOfBytesRead, &ovlOverlapped) { - let _ = GetLastError() - // TODO(compnerd) map windows error to errno - return Int(-1) - } - return Int(nNumberOfBytesRead) -} - -@inline(__always) -internal func pwrite( - _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int, _ offset: off_t -) -> Int { - let handle: intptr_t = _get_osfhandle(fd) - if handle == /* INVALID_HANDLE_VALUE */ -1 { return Int(EBADF) } - - // NOTE: this is a non-owning handle, do *not* call CloseHandle on it - let hFile: HANDLE = HANDLE(bitPattern: handle)! - - var ovlOverlapped: OVERLAPPED = OVERLAPPED() - ovlOverlapped.OffsetHigh = DWORD(UInt32(offset >> 32) & 0xffffffff) - ovlOverlapped.Offset = DWORD(UInt32(offset >> 0) & 0xffffffff) - - var nNumberOfBytesWritten: DWORD = 0 - if !WriteFile(hFile, buf, DWORD(nbyte), &nNumberOfBytesWritten, - &ovlOverlapped) { - let _ = GetLastError() - // TODO(compnerd) map windows error to errno - return Int(-1) - } - return Int(nNumberOfBytesWritten) -} - -#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 695f4e5b..00000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,8 +0,0 @@ -import XCTest - -import SystemTests - -var tests = [XCTestCaseEntry]() -tests += SystemTests.__allTests() - -XCTMain(tests) diff --git a/Tests/SystemTests/ErrnoTest.swift b/Tests/SystemTests/ErrnoTest.swift index 5c026b3f..767190ee 100644 --- a/Tests/SystemTests/ErrnoTest.swift +++ b/Tests/SystemTests/ErrnoTest.swift @@ -1,16 +1,25 @@ /* 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 - 2025 Apple Inc. and the Swift System project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information */ import XCTest + +#if SYSTEM_PACKAGE import SystemPackage +#else +import System +#endif + +#if os(Windows) +import WinSDK +#endif -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) final class ErrnoTest: XCTestCase { func testConstants() { XCTAssert(EPERM == Errno.notPermitted.rawValue) @@ -27,7 +36,9 @@ final class ErrnoTest: XCTestCase { XCTAssert(ENOMEM == Errno.noMemory.rawValue) XCTAssert(EACCES == Errno.permissionDenied.rawValue) XCTAssert(EFAULT == Errno.badAddress.rawValue) +#if !os(Windows) && !os(WASI) XCTAssert(ENOTBLK == Errno.notBlockDevice.rawValue) +#endif XCTAssert(EBUSY == Errno.resourceBusy.rawValue) XCTAssert(EEXIST == Errno.fileExists.rawValue) XCTAssert(EXDEV == Errno.improperLink.rawValue) @@ -37,8 +48,10 @@ final class ErrnoTest: XCTestCase { XCTAssert(EINVAL == Errno.invalidArgument.rawValue) XCTAssert(ENFILE == Errno.tooManyOpenFilesInSystem.rawValue) XCTAssert(EMFILE == Errno.tooManyOpenFiles.rawValue) +#if !os(Windows) XCTAssert(ENOTTY == Errno.inappropriateIOCTLForDevice.rawValue) XCTAssert(ETXTBSY == Errno.textFileBusy.rawValue) +#endif XCTAssert(EFBIG == Errno.fileTooLarge.rawValue) XCTAssert(ENOSPC == Errno.noSpace.rawValue) XCTAssert(ESPIPE == Errno.illegalSeek.rawValue) @@ -56,9 +69,17 @@ final class ErrnoTest: XCTestCase { XCTAssert(EPROTOTYPE == Errno.protocolWrongTypeForSocket.rawValue) XCTAssert(ENOPROTOOPT == Errno.protocolNotAvailable.rawValue) XCTAssert(EPROTONOSUPPORT == Errno.protocolNotSupported.rawValue) - XCTAssert(ESOCKTNOSUPPORT == Errno.socketTypeNotSupported.rawValue) +#if os(Windows) + XCTAssert(WSAESOCKTNOSUPPORT == Errno.socketTypeNotSupported.rawValue) + XCTAssert(WSAEOPNOTSUPP == Errno.notSupported.rawValue) + XCTAssert(WSAEPFNOSUPPORT == Errno.protocolFamilyNotSupported.rawValue) +#else XCTAssert(ENOTSUP == Errno.notSupported.rawValue) +#if !os(WASI) + XCTAssert(ESOCKTNOSUPPORT == Errno.socketTypeNotSupported.rawValue) XCTAssert(EPFNOSUPPORT == Errno.protocolFamilyNotSupported.rawValue) +#endif +#endif XCTAssert(EAFNOSUPPORT == Errno.addressFamilyNotSupported.rawValue) XCTAssert(EADDRINUSE == Errno.addressInUse.rawValue) XCTAssert(EADDRNOTAVAIL == Errno.addressNotAvailable.rawValue) @@ -70,24 +91,40 @@ final class ErrnoTest: XCTestCase { XCTAssert(ENOBUFS == Errno.noBufferSpace.rawValue) XCTAssert(EISCONN == Errno.socketIsConnected.rawValue) XCTAssert(ENOTCONN == Errno.socketNotConnected.rawValue) +#if os(Windows) + XCTAssert(WSAESHUTDOWN == Errno.socketShutdown.rawValue) +#elseif !os(WASI) XCTAssert(ESHUTDOWN == Errno.socketShutdown.rawValue) +#endif XCTAssert(ETIMEDOUT == Errno.timedOut.rawValue) XCTAssert(ECONNREFUSED == Errno.connectionRefused.rawValue) XCTAssert(ELOOP == Errno.tooManySymbolicLinkLevels.rawValue) XCTAssert(ENAMETOOLONG == Errno.fileNameTooLong.rawValue) +#if os(Windows) + XCTAssert(WSAEHOSTDOWN == Errno.hostIsDown.rawValue) +#elseif !os(WASI) XCTAssert(EHOSTDOWN == Errno.hostIsDown.rawValue) +#endif XCTAssert(EHOSTUNREACH == Errno.noRouteToHost.rawValue) XCTAssert(ENOTEMPTY == Errno.directoryNotEmpty.rawValue) -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN XCTAssert(EPROCLIM == Errno.tooManyProcesses.rawValue) #endif +#if os(Windows) + XCTAssert(WSAEUSERS == Errno.tooManyUsers.rawValue) + XCTAssert(WSAEDQUOT == Errno.diskQuotaExceeded.rawValue) + XCTAssert(WSAESTALE == Errno.staleNFSFileHandle.rawValue) +#else +#if !os(WASI) XCTAssert(EUSERS == Errno.tooManyUsers.rawValue) +#endif XCTAssert(EDQUOT == Errno.diskQuotaExceeded.rawValue) XCTAssert(ESTALE == Errno.staleNFSFileHandle.rawValue) +#endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN XCTAssert(EBADRPC == Errno.rpcUnsuccessful.rawValue) XCTAssert(ERPCMISMATCH == Errno.rpcVersionMismatch.rawValue) XCTAssert(EPROGUNAVAIL == Errno.rpcProgramUnavailable.rawValue) @@ -98,17 +135,22 @@ final class ErrnoTest: XCTestCase { XCTAssert(ENOLCK == Errno.noLocks.rawValue) XCTAssert(ENOSYS == Errno.noFunction.rawValue) -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) XCTAssert(EFTYPE == Errno.badFileTypeOrFormat.rawValue) XCTAssert(EAUTH == Errno.authenticationError.rawValue) XCTAssert(ENEEDAUTH == Errno.needAuthenticator.rawValue) +#endif + +#if SYSTEM_PACKAGE_DARWIN XCTAssert(EPWROFF == Errno.devicePowerIsOff.rawValue) XCTAssert(EDEVERR == Errno.deviceError.rawValue) #endif +#if !os(Windows) XCTAssert(EOVERFLOW == Errno.overflow.rawValue) +#endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN XCTAssert(EBADEXEC == Errno.badExecutable.rawValue) XCTAssert(EBADARCH == Errno.badCPUType.rawValue) XCTAssert(ESHLIBVERS == Errno.sharedLibraryVersionMismatch.rawValue) @@ -116,37 +158,60 @@ final class ErrnoTest: XCTestCase { #endif XCTAssert(ECANCELED == Errno.canceled.rawValue) +#if !os(Windows) XCTAssert(EIDRM == Errno.identifierRemoved.rawValue) XCTAssert(ENOMSG == Errno.noMessage.rawValue) +#endif XCTAssert(EILSEQ == Errno.illegalByteSequence.rawValue) -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN XCTAssert(ENOATTR == Errno.attributeNotFound.rawValue) #endif +#if !os(Windows) XCTAssert(EBADMSG == Errno.badMessage.rawValue) XCTAssert(EMULTIHOP == Errno.multiHop.rawValue) - XCTAssert(ENODATA == Errno.noData.rawValue) XCTAssert(ENOLINK == Errno.noLink.rawValue) + XCTAssert(EPROTO == Errno.protocolError.rawValue) +#endif + +#if !os(Windows) && !os(FreeBSD) && !os(WASI) + XCTAssert(ENODATA == Errno.noData.rawValue) XCTAssert(ENOSR == Errno.noStreamResources.rawValue) XCTAssert(ENOSTR == Errno.notStream.rawValue) - XCTAssert(EPROTO == Errno.protocolError.rawValue) XCTAssert(ETIME == Errno.timeout.rawValue) +#endif + XCTAssert(EOPNOTSUPP == Errno.notSupportedOnSocket.rawValue) // From headers but not man page +#if !os(WASI) // Would need to use _getConst func from CSystem XCTAssert(EWOULDBLOCK == Errno.wouldBlock.rawValue) +#endif +#if os(Windows) + XCTAssert(WSAETOOMANYREFS == Errno.tooManyReferences.rawValue) + XCTAssert(WSAEREMOTE == Errno.tooManyRemoteLevels.rawValue) +#elseif !os(WASI) XCTAssert(ETOOMANYREFS == Errno.tooManyReferences.rawValue) XCTAssert(EREMOTE == Errno.tooManyRemoteLevels.rawValue) +#endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN XCTAssert(ENOPOLICY == Errno.noSuchPolicy.rawValue) #endif +#if !os(Windows) XCTAssert(ENOTRECOVERABLE == Errno.notRecoverable.rawValue) XCTAssert(EOWNERDEAD == Errno.previousOwnerDied.rawValue) +#endif + +#if os(FreeBSD) + XCTAssert(ENOTCAPABLE == Errno.notCapable.rawValue) + XCTAssert(ECAPMODE == Errno.capabilityMode.rawValue) + XCTAssert(EINTEGRITY == Errno.integrityCheckFailed.rawValue) +#endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN XCTAssert(EQFULL == Errno.outputQueueFull.rawValue) XCTAssert(ELAST == Errno.lastErrnoValue.rawValue) #endif diff --git a/Tests/SystemTests/FileDescriptorExtras.swift b/Tests/SystemTests/FileDescriptorExtras.swift new file mode 100644 index 00000000..b72816fa --- /dev/null +++ b/Tests/SystemTests/FileDescriptorExtras.swift @@ -0,0 +1,25 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +extension FileDescriptor { + internal func fileSize( + retryOnInterrupt: Bool = true + ) throws -> Int64 { + let current = try seek(offset: 0, from: .current) + let size = try seek(offset: 0, from: .end) + try seek(offset: current, from: .start) + return size + } +} diff --git a/Tests/SystemTests/FileModeTests.swift b/Tests/SystemTests/FileModeTests.swift new file mode 100644 index 00000000..46c3a5d6 --- /dev/null +++ b/Tests/SystemTests/FileModeTests.swift @@ -0,0 +1,136 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) + +import Testing + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@Suite("FileMode") +private struct FileModeTests { + + @available(System 99, *) + @Test func basics() async throws { + var mode = FileMode(rawValue: S_IFREG | 0o644) // Regular file, rw-r--r-- + #expect(mode.type == .regular) + #expect(mode.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + + mode.type = .directory // Directory, rw-r--r-- + #expect(mode.type == .directory) + #expect(mode.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + + mode.permissions.insert([.ownerExecute, .groupExecute, .otherExecute]) // Directory, rwxr-xr-x + #expect(mode.type == .directory) + #expect(mode.permissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]) + + mode.type = .symbolicLink // Symbolic link, rwxr-xr-x + #expect(mode.type == .symbolicLink) + #expect(mode.permissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]) + + let mode1 = FileMode(rawValue: S_IFLNK | 0o755) // Symbolic link, rwxr-xr-x + let mode2 = FileMode(type: .symbolicLink, permissions: [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]) + #expect(mode == mode1) + #expect(mode1 == mode2) + + mode.permissions.remove([.otherReadExecute]) // Symbolic link, rwxr-x--- + #expect(mode.permissions == [.ownerReadWriteExecute, .groupReadExecute]) + #expect(mode != mode1) + #expect(mode != mode2) + #expect(mode.type == mode1.type) + #expect(mode.type == mode2.type) + } + + @available(System 99, *) + @Test func invalidInput() async throws { + // No permissions, all other bits set + var invalidMode = FileMode(rawValue: ~0o7777) + #expect(invalidMode.permissions.isEmpty) + #expect(invalidMode.type != .directory) + #expect(invalidMode.type != .characterSpecial) + #expect(invalidMode.type != .blockSpecial) + #expect(invalidMode.type != .regular) + #expect(invalidMode.type != .fifo) + #expect(invalidMode.type != .symbolicLink) + #expect(invalidMode.type != .socket) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(invalidMode.type != .whiteout) + #endif + + // All file-type bits set + invalidMode = FileMode(rawValue: S_IFMT) + #expect(invalidMode.type != .directory) + #expect(invalidMode.type != .characterSpecial) + #expect(invalidMode.type != .blockSpecial) + #expect(invalidMode.type != .regular) + #expect(invalidMode.type != .fifo) + #expect(invalidMode.type != .symbolicLink) + #expect(invalidMode.type != .socket) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(invalidMode.type != .whiteout) + #endif + + // FileMode(type:permissions:) masks its inputs so + // they don't accidentally modify the other bits. + let emptyPermissions = FileMode(type: FileType(rawValue: ~0), permissions: []) + #expect(emptyPermissions.permissions.isEmpty) + #expect(emptyPermissions.type == FileType(rawValue: S_IFMT)) + #expect(emptyPermissions == invalidMode) + + let regularFile = FileMode(type: .regular, permissions: FilePermissions(rawValue: ~0)) + #expect(regularFile.type == .regular) + #expect(regularFile.permissions == FilePermissions(rawValue: 0o7777)) + #expect(regularFile.permissions == [ + .ownerReadWriteExecute, + .groupReadWriteExecute, + .otherReadWriteExecute, + .setUserID, .setGroupID, .saveText + ]) + + // Setting properties should not modify the other bits, either. + var mode = FileMode(rawValue: 0) + mode.type = FileType(rawValue: ~0) + #expect(mode.type == FileType(rawValue: S_IFMT)) + #expect(mode.permissions.isEmpty) + + mode.type.rawValue = 0 + #expect(mode.type == FileType(rawValue: 0)) + #expect(mode.permissions.isEmpty) + + mode.permissions = FilePermissions(rawValue: ~0) + #expect(mode.permissions == FilePermissions(rawValue: 0o7777)) + #expect(mode.type == FileType(rawValue: 0)) + + mode.permissions = [] + #expect(mode.permissions.isEmpty) + #expect(mode.type == FileType(rawValue: 0)) + } + +} +#endif diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index b3932ddf..9dd4c2b1 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -1,17 +1,28 @@ /* 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 - 2025 Apple Inc. and the Swift System project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information */ import XCTest -import SystemPackage -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif +#if canImport(Android) +import Android +#elseif os(WASI) +import CSystem +#endif + +@available(System 0.0.1, *) final class FileOperationsTest: XCTestCase { + #if ENABLE_MOCKING && !os(WASI) // Would need to use _getConst funcs from CSystem func testSyscalls() { let fd = FileDescriptor(rawValue: 1) @@ -24,13 +35,13 @@ final class FileOperationsTest: XCTestCase { let writeBufAddr = writeBuf.baseAddress let syscallTestCases: Array = [ - MockTestCase(name: "open", "a path", O_RDWR | O_APPEND, interruptable: true) { + MockTestCase(name: "open", .interruptable, "a path", O_RDWR | O_APPEND) { retryOnInterrupt in _ = try FileDescriptor.open( "a path", .readWrite, options: [.append], retryOnInterrupt: retryOnInterrupt) }, - MockTestCase(name: "open", "a path", O_WRONLY | O_CREAT | O_APPEND, 0o777, interruptable: true) { + MockTestCase(name: "open", .interruptable, "a path", O_WRONLY | O_CREAT | O_APPEND, 0o777) { retryOnInterrupt in _ = try FileDescriptor.open( "a path", .writeOnly, options: [.create, .append], @@ -38,77 +49,234 @@ final class FileOperationsTest: XCTestCase { retryOnInterrupt: retryOnInterrupt) }, - MockTestCase(name: "read", rawFD, bufAddr, bufCount, interruptable: true) { + MockTestCase(name: "read", .interruptable, rawFD, bufAddr, bufCount) { retryOnInterrupt in _ = try fd.read(into: rawBuf, retryOnInterrupt: retryOnInterrupt) }, - MockTestCase(name: "pread", rawFD, bufAddr, bufCount, 5, interruptable: true) { + MockTestCase(name: "pread", .interruptable, rawFD, bufAddr, bufCount, 5) { retryOnInterrupt in _ = try fd.read(fromAbsoluteOffset: 5, into: rawBuf, retryOnInterrupt: retryOnInterrupt) }, - MockTestCase(name: "lseek", rawFD, -2, SEEK_END, interruptable: false) { + MockTestCase(name: "lseek", .noInterrupt, rawFD, -2, SEEK_END) { _ in _ = try fd.seek(offset: -2, from: .end) }, - MockTestCase(name: "write", rawFD, writeBufAddr, bufCount, interruptable: true) { + MockTestCase(name: "write", .interruptable, rawFD, writeBufAddr, bufCount) { retryOnInterrupt in _ = try fd.write(writeBuf, retryOnInterrupt: retryOnInterrupt) }, - MockTestCase(name: "pwrite", rawFD, writeBufAddr, bufCount, 7, interruptable: true) { + MockTestCase(name: "pwrite", .interruptable, rawFD, writeBufAddr, bufCount, 7) { retryOnInterrupt in _ = try fd.write(toAbsoluteOffset: 7, writeBuf, retryOnInterrupt: retryOnInterrupt) }, - MockTestCase(name: "close", rawFD, interruptable: false) { + MockTestCase(name: "close", .noInterrupt, rawFD) { _ in _ = try fd.close() }, + MockTestCase(name: "dup", .interruptable, rawFD) { retryOnInterrupt in + _ = try fd.duplicate(retryOnInterrupt: retryOnInterrupt) + }, + + MockTestCase(name: "dup2", .interruptable, rawFD, 42) { retryOnInterrupt in + _ = try fd.duplicate(as: FileDescriptor(rawValue: 42), + retryOnInterrupt: retryOnInterrupt) + }, ] for test in syscallTestCases { test.runAllTests() } } + #endif // ENABLE_MOCKING && !os(WASI) + + func testWriteFromEmptyBuffer() throws { + #if os(Windows) + let fd = try FileDescriptor.open(FilePath("NUL"), .writeOnly) + #else + let fd = try FileDescriptor.open(FilePath("/dev/null"), .writeOnly) + #endif + let written1 = try fd.write(toAbsoluteOffset: 0, .init(start: nil, count: 0)) + XCTAssertEqual(written1, 0) + + let pointer = UnsafeMutableRawPointer.allocate(byteCount: 8, alignment: 8) + defer { pointer.deallocate() } + let empty = UnsafeRawBufferPointer(start: pointer, count: 0) + let written2 = try fd.write(toAbsoluteOffset: 0, empty) + XCTAssertEqual(written2, 0) + } + + #if os(Windows) + // Generate a file containing random bytes; this should not be used + // for cryptography, it's just for testing. + func generateRandomData(at path: FilePath, count: Int) throws { + let fd = try FileDescriptor.open(path, .readWrite, + options: [.create, .truncate]) + defer { + try! fd.close() + } + let data = [UInt8]( + sequence(first: 0, + next: { + _ in UInt8.random(in: UInt8.min...UInt8.max) + }).dropFirst().prefix(count) + ) + + try data.withUnsafeBytes { + _ = try fd.write($0) + } + } + #endif + + func testReadToEmptyBuffer() throws { + try withTemporaryFilePath(basename: "testReadToEmptyBuffer") { path in + #if os(Windows) + // Windows doesn't have an equivalent to /dev/random, so generate + // some random bytes and write them to a file for the next step. + let randomPath = path.appending("random.txt") + try generateRandomData(at: randomPath, count: 16) + let fd = try FileDescriptor.open(randomPath, .readOnly) + #else // !os(Windows) + let fd = try FileDescriptor.open(FilePath("/dev/random"), .readOnly) + #endif + let read1 = try fd.read(fromAbsoluteOffset: 0, into: .init(start: nil, count: 0)) + XCTAssertEqual(read1, 0) + + let pointer = UnsafeMutableRawPointer.allocate(byteCount: 8, alignment: 8) + defer { pointer.deallocate() } + let empty = UnsafeMutableRawBufferPointer(start: pointer, count: 0) + let read2 = try fd.read(fromAbsoluteOffset: 0, into: empty) + XCTAssertEqual(read2, 0) + } + } func testHelpers() { // TODO: Test writeAll, writeAll(toAbsoluteOffset), closeAfter } + #if !os(WASI) // WASI has no pipe + func testAdHocPipe() throws { + // Ad-hoc test testing `Pipe` functionality. + // We cannot test `Pipe` using `MockTestCase` because it calls `pipe` with a pointer to an array local to the `Pipe`, the address of which we do not know prior to invoking `Pipe`. + let pipe = try FileDescriptor.pipe() + try pipe.readEnd.closeAfter { + try pipe.writeEnd.closeAfter { + var abc = "abc" + try abc.withUTF8 { + _ = try pipe.writeEnd.write(UnsafeRawBufferPointer($0)) + } + let readLen = 3 + let readBytes = try Array(unsafeUninitializedCapacity: readLen) { buf, count in + count = try pipe.readEnd.read(into: UnsafeMutableRawBufferPointer(buf)) + } + XCTAssertEqual(readBytes, Array(abc.utf8)) + } + } + } + #endif + func testAdHocOpen() { // Ad-hoc test touching a file system. do { // TODO: Test this against a virtual in-memory file system - let fd = try FileDescriptor.open("/tmp/b.txt", .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) - try fd.closeAfter { - try fd.writeAll("abc".utf8) - var def = "def" - try def.withUTF8 { - _ = try fd.write(UnsafeRawBufferPointer($0)) - } - try fd.seek(offset: 1, from: .start) + try withTemporaryFilePath(basename: "testAdhocOpen") { path in + let fd = try FileDescriptor.open(path.appending("b.txt"), .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) + try fd.closeAfter { + try fd.writeAll("abc".utf8) + var def = "def" + try def.withUTF8 { + _ = try fd.write(UnsafeRawBufferPointer($0)) + } + try fd.seek(offset: 1, from: .start) - let readLen = 3 - let readBytes = try Array(unsafeUninitializedCapacity: readLen) { (buf, count) in - count = try fd.read(into: UnsafeMutableRawBufferPointer(buf)) - } - let preadBytes = try Array(unsafeUninitializedCapacity: readLen) { (buf, count) in - count = try fd.read(fromAbsoluteOffset: 1, into: UnsafeMutableRawBufferPointer(buf)) - } + let readLen = 3 + let readBytes = try Array(unsafeUninitializedCapacity: readLen) { (buf, count) in + count = try fd.read(into: UnsafeMutableRawBufferPointer(buf)) + } + let preadBytes = try Array(unsafeUninitializedCapacity: readLen) { (buf, count) in + count = try fd.read(fromAbsoluteOffset: 1, into: UnsafeMutableRawBufferPointer(buf)) + } - XCTAssertEqual(readBytes.first!, "b".utf8.first!) - XCTAssertEqual(readBytes, preadBytes) + XCTAssertEqual(readBytes.first!, "b".utf8.first!) + XCTAssertEqual(readBytes, preadBytes) - // TODO: seek + // TODO: seek + } } } catch let err as Errno { print("caught \(err))") // Should we assert? I'd be interested in knowing if this happened XCTAssert(false) } catch { - fatalError() + fatalError("FATAL: `testAdHocOpen`") + } + } + + #if ENABLE_MOCKING + func testGithubIssues() { + // https://github.com/apple/swift-system/issues/26 + #if os(WASI) + let openOptions = _getConst_O_WRONLY() | _getConst_O_CREAT() + #else + let openOptions = O_WRONLY | O_CREAT + #endif + let issue26 = MockTestCase( + name: "open", .interruptable, "a path", openOptions, 0o020 + ) { + retryOnInterrupt in + _ = try FileDescriptor.open( + "a path", .writeOnly, options: [.create], + permissions: [.groupWrite], + retryOnInterrupt: retryOnInterrupt) + } + issue26.runAllTests() + } + #endif // ENABLE_MOCKING + + func testResizeFile() throws { + try withTemporaryFilePath(basename: "testResizeFile") { path in + let fd = try FileDescriptor.open(path.appending("\(UUID().uuidString).txt"), .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) + try fd.closeAfter { + // File should be empty initially. + XCTAssertEqual(try fd.fileSize(), 0) + // Write 3 bytes. + try fd.writeAll("abc".utf8) + // File should now be 3 bytes. + XCTAssertEqual(try fd.fileSize(), 3) + // Resize to 6 bytes. + try fd.resize(to: 6) + // File should now be 6 bytes. + XCTAssertEqual(try fd.fileSize(), 6) + // Read in the 6 bytes. + let readBytes = try Array(unsafeUninitializedCapacity: 6) { (buf, count) in + try fd.seek(offset: 0, from: .start) + // Should have read all 6 bytes. + count = try fd.read(into: UnsafeMutableRawBufferPointer(buf)) + XCTAssertEqual(count, 6) + } + // First 3 bytes should be unaffected by resize. + XCTAssertEqual(Array(readBytes[..<3]), Array("abc".utf8)) + // Extension should be padded with zeros. + XCTAssertEqual(Array(readBytes[3...]), Array(repeating: 0, count: 3)) + // File should still be 6 bytes. + XCTAssertEqual(try fd.fileSize(), 6) + // Resize to 2 bytes. + try fd.resize(to: 2) + // File should now be 2 bytes. + XCTAssertEqual(try fd.fileSize(), 2) + // Read in file with a buffer big enough for 6 bytes. + let readBytesAfterTruncation = try Array(unsafeUninitializedCapacity: 6) { (buf, count) in + try fd.seek(offset: 0, from: .start) + count = try fd.read(into: UnsafeMutableRawBufferPointer(buf)) + // Should only have read 2 bytes. + XCTAssertEqual(count, 2) + } + // Written content was trunctated. + XCTAssertEqual(readBytesAfterTruncation, Array("ab".utf8)) + } } } } diff --git a/Tests/SystemTests/FileOperationsTestWindows.swift b/Tests/SystemTests/FileOperationsTestWindows.swift new file mode 100644 index 00000000..7b87b354 --- /dev/null +++ b/Tests/SystemTests/FileOperationsTestWindows.swift @@ -0,0 +1,247 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 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 +*/ + +import XCTest + +#if os(Windows) + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +import WinSDK + +@available(iOS 8, *) +final class FileOperationsTestWindows: XCTestCase { + private let r = ACCESS_MASK( + FILE_READ_ATTRIBUTES + | FILE_READ_DATA + | FILE_READ_EA + | STANDARD_RIGHTS_READ + | SYNCHRONIZE + ) + private let w = ACCESS_MASK( + FILE_APPEND_DATA + | FILE_WRITE_ATTRIBUTES + | FILE_WRITE_DATA + | FILE_WRITE_EA + | STANDARD_RIGHTS_WRITE + | SYNCHRONIZE + ) + private let x = ACCESS_MASK( + FILE_EXECUTE + | FILE_READ_ATTRIBUTES + | STANDARD_RIGHTS_EXECUTE + | SYNCHRONIZE + ) + + private struct Test { + var permissions: CModeT + var ownerAccess: ACCESS_MASK + var groupAccess: ACCESS_MASK + var otherAccess: ACCESS_MASK + + init(_ permissions: CModeT, + _ ownerAccess: ACCESS_MASK, + _ groupAccess: ACCESS_MASK, + _ otherAccess: ACCESS_MASK) { + self.permissions = permissions + self.ownerAccess = ownerAccess + self.groupAccess = groupAccess + self.otherAccess = otherAccess + } + } + + /// Retrieve the owner, group and other access masks for a given file. + /// + /// - Parameters: + /// - path: The path to the file to inspect + /// - Returns: A tuple of ACCESS_MASK values. + func getAccessMasks( + path: FilePath + ) -> (ACCESS_MASK, ACCESS_MASK, ACCESS_MASK) { + var SIDAuthWorld = SID_IDENTIFIER_AUTHORITY(Value: (0, 0, 0, 0, 0, 1)) + var psidEveryone: PSID? = nil + + XCTAssert(AllocateAndInitializeSid(&SIDAuthWorld, 1, + DWORD(SECURITY_WORLD_RID), + 0, 0, 0, 0, 0, 0, 0, + &psidEveryone)) + defer { + FreeSid(psidEveryone) + } + + var everyone = TRUSTEE_W( + pMultipleTrustee: nil, + MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_GROUP, + ptstrName: + psidEveryone!.assumingMemoryBound(to: CInterop.PlatformChar.self) + ) + + return path.withPlatformString { objectName in + var psidOwner: PSID? = nil + var psidGroup: PSID? = nil + var pDacl: PACL? = nil + var pSD: PSECURITY_DESCRIPTOR? = nil + + XCTAssertEqual(GetNamedSecurityInfoW( + objectName, + SE_FILE_OBJECT, + SECURITY_INFORMATION( + DACL_SECURITY_INFORMATION + | GROUP_SECURITY_INFORMATION + | OWNER_SECURITY_INFORMATION + ), + &psidOwner, + &psidGroup, + &pDacl, + nil, + &pSD), DWORD(ERROR_SUCCESS)) + defer { + LocalFree(pSD) + } + + var owner = TRUSTEE_W( + pMultipleTrustee: nil, + MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_USER, + ptstrName: + psidOwner!.assumingMemoryBound(to: CInterop.PlatformChar.self) + ) + var group = TRUSTEE_W( + pMultipleTrustee: nil, + MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_GROUP, + ptstrName: + psidGroup!.assumingMemoryBound(to: CInterop.PlatformChar.self) + ) + + var ownerAccess = ACCESS_MASK(0) + var groupAccess = ACCESS_MASK(0) + var otherAccess = ACCESS_MASK(0) + + XCTAssertEqual(GetEffectiveRightsFromAclW( + pDacl, + &owner, + &ownerAccess), DWORD(ERROR_SUCCESS)) + XCTAssertEqual(GetEffectiveRightsFromAclW( + pDacl, + &group, + &groupAccess), DWORD(ERROR_SUCCESS)) + XCTAssertEqual(GetEffectiveRightsFromAclW( + pDacl, + &everyone, + &otherAccess), DWORD(ERROR_SUCCESS)) + + return (ownerAccess, groupAccess, otherAccess) + } + } + + private func runTests(_ tests: [Test], at path: FilePath) throws { + for test in tests { + let octal = String(test.permissions, radix: 8) + let testPath = path.appending("test-\(octal).txt") + let fd = try FileDescriptor.open( + testPath, + .readWrite, + options: [.create, .truncate], + permissions: FilePermissions(rawValue: test.permissions) + ) + _ = try fd.closeAfter { + try fd.writeAll("Hello World".utf8) + } + + let (ownerAccess, groupAccess, otherAccess) + = getAccessMasks(path: testPath) + + XCTAssertEqual(ownerAccess, test.ownerAccess) + XCTAssertEqual(groupAccess, test.groupAccess) + XCTAssertEqual(otherAccess, test.otherAccess) + } + } + + /// Test that the umask works properly + func testUmask() throws { + // See https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/persistent-storage#permissions + try XCTSkipIf(NSUserName() == "ContainerAdministrator", "containers use a different permission model") + + // Default mask should be 0o022 + XCTAssertEqual(FilePermissions.creationMask, [.groupWrite, .otherWrite]) + + try withTemporaryFilePath(basename: "testUmask") { path in + let tests = [ + Test(0o000, 0, 0, 0), + Test(0o700, r|w|x, 0, 0), + Test(0o770, r|w|x, r|x, 0), + Test(0o777, r|w|x, r|x, r|x) + ] + + try runTests(tests, at: path) + } + + try FilePermissions.withCreationMask([.groupWrite, .groupExecute, + .otherWrite, .otherExecute]) { + try withTemporaryFilePath(basename: "testUmask") { path in + let tests = [ + Test(0o000, 0, 0, 0), + Test(0o700, r|w|x, 0, 0), + Test(0o770, r|w|x, r, 0), + Test(0o777, r|w|x, r, r) + ] + + try runTests(tests, at: path) + } + } + } + + /// Test that setting permissions on a file works as expected + func testPermissions() throws { + // See https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/persistent-storage#permissions + try XCTSkipIf(NSUserName() == "ContainerAdministrator", "containers use a different permission model") + + try FilePermissions.withCreationMask([]) { + try withTemporaryFilePath(basename: "testPermissions") { path in + let tests = [ + Test(0o000, 0, 0, 0), + + Test(0o400, r, 0, 0), + Test(0o200, w, 0, 0), + Test(0o100, x, 0, 0), + Test(0o040, 0, r, 0), + Test(0o020, 0, w, 0), + Test(0o010, 0, x, 0), + Test(0o004, 0, 0, r), + Test(0o002, 0, 0, w), + Test(0o001, 0, 0, x), + + Test(0o700, r|w|x, 0, 0), + Test(0o770, r|w|x, r|w|x, 0), + Test(0o777, r|w|x, r|w|x, r|w|x), + + Test(0o755, r|w|x, r|x, r|x), + Test(0o644, r|w, r, r), + + Test(0o007, 0, 0, r|w|x), + Test(0o070, 0, r|w|x, 0), + Test(0o077, 0, r|w|x, r|w|x), + ] + + try runTests(tests, at: path) + } + } + } +} + +#endif // os(Windows) diff --git a/Tests/SystemTests/FilePathTest.swift b/Tests/SystemTests/FilePathTest.swift deleted file mode 100644 index 441d9a0a..00000000 --- a/Tests/SystemTests/FilePathTest.swift +++ /dev/null @@ -1,76 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 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 -*/ - -import XCTest -import SystemPackage - -extension UnsafePointer where Pointee == UInt8 { - internal var _asCChar: UnsafePointer { - UnsafeRawPointer(self).assumingMemoryBound(to: CChar.self) - } -} - -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -func filePathFromUnterminatedBytes(_ bytes: S) -> FilePath where S.Element == UInt8 { - var array = Array(bytes) - assert(array.last != 0, "already null terminated") - array += [0] - - return array.withUnsafeBufferPointer { - FilePath(cString: $0.baseAddress!._asCChar) - } -} -let invalidBytes: [UInt8] = [0x2F, 0x61, 0x2F, 0x62, 0x2F, 0x83] - -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -final class FilePathTest: XCTestCase { - struct TestPath { - let filePath: FilePath - let string: String - let validUTF8: Bool - } - - let testPaths: [TestPath] = [ - // empty - TestPath(filePath: FilePath(), string: String(), validUTF8: true), - - // valid ascii - TestPath(filePath: "/a/b/c", string: "/a/b/c", validUTF8: true), - - // valid utf8 - TestPath(filePath: "/あ/🧟‍♀️", string: "/あ/🧟‍♀️", validUTF8: true), - - // invalid utf8 - TestPath(filePath: filePathFromUnterminatedBytes(invalidBytes), string: String(decoding: invalidBytes, as: UTF8.self), validUTF8: false), - ] - - func testFilePath() { - - XCTAssertEqual(0, FilePath().length) - - for testPath in testPaths { - - XCTAssertEqual(testPath.string, String(decoding: testPath.filePath)) - - if testPath.validUTF8 { - XCTAssertEqual(testPath.filePath, FilePath(testPath.string)) - XCTAssertEqual(testPath.string, String(validatingUTF8: testPath.filePath)) - } else { - XCTAssertNotEqual(testPath.filePath, FilePath(testPath.string)) - XCTAssertNil(String(validatingUTF8: testPath.filePath)) - } - - testPath.filePath.withCString { - XCTAssertEqual(testPath.string, String(cString: $0)) - XCTAssertEqual(testPath.filePath, FilePath(cString: $0)) - } - } - } -} - diff --git a/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift b/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift new file mode 100644 index 00000000..675b5330 --- /dev/null +++ b/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift @@ -0,0 +1,292 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@available(System 0.0.2, *) +struct TestPathComponents: TestCase { + var path: FilePath + var expectedRoot: FilePath.Root? + var expectedComponents: [FilePath.Component] + + var pathComponents: Array { Array(path.components) } + + let file: StaticString + let line: UInt + + func failureMessage(_ reason: String?) -> String { + """ + + Fail \(reason ?? "") + path: \(path) + components: \(pathComponents)) + expected: \(expectedComponents) + """ + } + + init( + _ path: FilePath, + root: FilePath.Root?, + _ components: C, + file: StaticString = #file, line: UInt = #line + ) where C.Element == FilePath.Component { + self.path = path + self.expectedRoot = root + self.expectedComponents = Array(components) + self.file = file + self.line = line + } + + func testComponents() { + expectEqual(expectedRoot, path.root) + expectEqualSequence( + expectedComponents, Array(path.components), "testComponents()") + } + + func testBidi() { + expectEqualSequence( + expectedComponents.reversed(), path.components.reversed(), "reversed()") + expectEqualSequence( + path.components, path.components.reversed().reversed(), + "reversed().reversed()") + for i in 0 ..< path.components.count { + expectEqualSequence( + expectedComponents.dropLast(i), path.components.dropLast(i), "dropLast") + expectEqualSequence( + expectedComponents.suffix(i), path.components.suffix(i), "suffix") + } + } + + func testRRC() { + // TODO: programmatic tests showing parity with Array + } + + func testModify() { + if path.root == nil { + let rootedPath = FilePath(root: "/", path.components) + expectNotEqual(rootedPath, path) + var pathCopy = path + expectEqual(path, pathCopy) + pathCopy.components = rootedPath.components + expectNil(pathCopy.root, "components.set doesn't assign root") + expectEqual(path, pathCopy) + } else { + let rootlessPath = FilePath(root: nil, path.components) + var pathCopy = path + expectEqual(path, pathCopy) + pathCopy.components = rootlessPath.components + expectNotNil(pathCopy.root, "components.set preserves root") + expectEqual(path, pathCopy) + } + } + + func runAllTests() { + testComponents() + testBidi() + testRRC() + testModify() + } +} + +@available(System 0.0.2, *) +final class FilePathComponentsTest: XCTestCase { + func testAdHocRRC() { + var path: FilePath = "/usr/local/bin" + + func expect( + _ s: String, + _ file: StaticString = #filePath, + _ line: UInt = #line + ) { + if path == FilePath(s) { return } + + defer { print("expected: \(s), actual: \(path)") } + XCTAssert(false, file: file, line: line) + } + + // Run `body`, restoring `path` afterwards + func restoreAfter( + body: () -> () + ) { + let copy = path + defer { path = copy } + body() + } + + // Interior removal keeps path prefix intact, if there is one + restoreAfter { + path = "prefix//middle1/middle2////suffix" + let suffix = path.components.indices.last! + path.components.removeSubrange(.. = [ + TestPathComponents("", root: nil, []), + TestPathComponents("/", root: "/", []), + TestPathComponents("foo", root: nil, ["foo"]), + TestPathComponents("foo/", root: nil, ["foo"]), + TestPathComponents("/foo", root: "/", ["foo"]), + TestPathComponents("foo/bar", root: nil, ["foo", "bar"]), + TestPathComponents("foo/bar/", root: nil, ["foo", "bar"]), + TestPathComponents("/foo/bar", root: "/", ["foo", "bar"]), + TestPathComponents("/foo///bar", root: "/", ["foo", "bar"]), + TestPathComponents("foo/bar/", root: nil, ["foo", "bar"]), + TestPathComponents("foo///bar/baz/", root: nil, ["foo", "bar", "baz"]), + TestPathComponents("./", root: nil, ["."]), + TestPathComponents("./..", root: nil, [".", ".."]), + TestPathComponents("/./..//", root: "/", [".", ".."]), + ] +#if !os(Windows) + testPaths.append(contentsOf:[ + TestPathComponents("///foo//", root: "/", ["foo"]), + TestPathComponents("//foo///bar/baz/", root: "/", ["foo", "bar", "baz"]) + ]) +#else + // On Windows, these are UNC paths + testPaths.append(contentsOf:[ + TestPathComponents("///foo//", root: "///foo//", []), + TestPathComponents("//foo///bar/baz/", root: "//foo//", ["bar", "baz"]) + ]) +#endif + testPaths.forEach { + $0.runAllTests() + } + } + + func testSeparatorNormalization() { + var paths: Array = [ + "/a/b", + "/a/b/", + "/a//b/", + "/a/b//", + "/a/b////", + "/a////b/", + ] + #if !os(Windows) + paths.append("//a/b") + paths.append("///a/b") + paths.append("///a////b") + paths.append("///a////b///") + #endif + + for path in paths { + var path = path + path._normalizeSeparators() + XCTAssertEqual(path, "/a/b") + } + } +} + +// TODO: Test hashValue and equatable for equal components, i.e. make +// sure indices are not part of the hash. diff --git a/Tests/SystemTests/FilePathTests/FilePathDecodable.swift b/Tests/SystemTests/FilePathTests/FilePathDecodable.swift new file mode 100644 index 00000000..b88b0299 --- /dev/null +++ b/Tests/SystemTests/FilePathTests/FilePathDecodable.swift @@ -0,0 +1,129 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c)2024 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 + */ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@available(System 0.0.1, *) +final class FilePathDecodableTest: XCTestCase { + func testInvalidFilePath() { + // _storage is a valid SystemString, but the invariants of FilePath are + // violated (specifically, _storage is not normal). + let input: [UInt8] = [ + 123, 34, 95,115,116,111,114, 97,103,101, 34, 58,123, 34,110,117,108,108, + 84,101,114,109,105,110, 97,116,101,100, 83,116,111,114, 97,103,101, 34, + 58, 91, 49, 48, 57, 44, 45, 55, 54, 44, 53, 53, 44, 55, 49, 44, 49, 52, + 44, 53, 57, 44, 45, 49, 49, 50, 44, 45, 56, 52, 44, 52, 50, 44, 45, 55, + 48, 44, 45, 49, 48, 52, 44, 55, 51, 44, 45, 54, 44, 50, 44, 53, 55, 44, + 54, 50, 44, 45, 56, 55, 44, 45, 53, 44, 45, 54, 53, 44, 45, 51, 57, 44, + 45, 49, 48, 57, 44, 45, 55, 54, 44, 51, 48, 44, 53, 50, 44, 45, 56, 50, + 44, 45, 54, 48, 44, 45, 50, 44, 56, 53, 44, 49, 50, 51, 44, 45, 56, 52, + 44, 45, 53, 56, 44, 49, 49, 52, 44, 49, 44, 45, 49, 49, 54, 44, 56, 48, + 44, 49, 48, 52, 44, 45, 55, 56, 44, 45, 52, 53, 44, 49, 54, 44, 45, 52, + 54, 44, 55, 44, 49, 49, 56, 44, 45, 50, 52, 44, 54, 50, 44, 54, 52, 44, + 45, 52, 49, 44, 45, 49, 48, 51, 44, 53, 44, 45, 55, 53, 44, 50, 50, 44, + 45, 49, 48, 53, 44, 45, 49, 54, 44, 52, 55, 44, 52, 55, 44, 49, 50, 52, + 44, 45, 53, 55, 44, 53, 51, 44, 49, 49, 49, 44, 49, 53, 44, 45, 50, 55, + 44, 54, 54, 44, 45, 49, 54, 44, 49, 48, 50, 44, 49, 48, 54, 44, 49, 51, + 44, 49, 48, 53, 44, 45, 49, 49, 50, 44, 55, 56, 44, 45, 53, 48, 44, 50, + 48, 44, 56, 44, 45, 50, 55, 44, 52, 52, 44, 52, 44, 56, 44, 54, 53, 44, + 50, 51, 44, 57, 55, 44, 45, 50, 56, 44, 56, 56, 44, 52, 50, 44, 45, 51, + 54, 44, 45, 50, 51, 44, 49, 48, 51, 44, 57, 57, 44, 45, 53, 56, 44, 45, + 49, 49, 48, 44, 45, 53, 52, 44, 45, 49, 49, 55, 44, 45, 57, 52, 44, 45, + 55, 50, 44, 50, 57, 44, 45, 50, 52, 44, 45, 56, 52, 44, 53, 55, 44, 45, + 49, 50, 54, 44, 52, 52, 44, 55, 53, 44, 55, 54, 44, 52, 57, 44, 45, 52, + 49, 44, 45, 50, 53, 44, 50, 52, 44, 45, 49, 50, 54, 44, 55, 44, 50, 56, + 44, 45, 52, 56, 44, 56, 55, 44, 51, 49, 44, 45, 49, 49, 53, 44, 55, 44, + 45, 54, 48, 44, 53, 57, 44, 49, 51, 44, 55, 57, 44, 53, 48, 44, 45, 57, + 54, 44, 45, 50, 44, 45, 50, 52, 44, 45, 57, 49, 44, 55, 49, 44, 45, 49, + 50, 53, 44, 52, 50, 44, 45, 56, 52, 44, 52, 44, 53, 57, 44, 49, 50, 53, + 44, 49, 50, 49, 44, 45, 50, 54, 44, 45, 49, 50, 44, 45, 49, 48, 53, 44, + 53, 54, 44, 49, 49, 48, 44, 49, 52, 44, 45, 49, 48, 52, 44, 45, 53, 50, + 44, 45, 53, 56, 44, 45, 54, 44, 45, 50, 54, 44, 45, 52, 55, 44, 53, 57, + 44, 52, 50, 44, 49, 50, 51, 44, 52, 52, 44, 45, 57, 50, 44, 45, 50, 57, + 44, 45, 51, 54, 44, 45, 54, 50, 44, 50, 54, 44, 45, 49, 55, 44, 45, 49, + 48, 44, 45, 56, 49, 44, 54, 49, 44, 52, 55, 44, 45, 57, 52, 44, 45, 49, + 48, 54, 44, 49, 53, 44, 49, 48, 48, 44, 45, 49, 50, 49, 44, 45, 49, 49, + 49, 44, 51, 44, 45, 57, 44, 52, 54, 44, 45, 55, 48, 44, 45, 49, 57, 44, + 52, 56, 44, 45, 49, 50, 44, 45, 57, 49, 44, 45, 50, 48, 44, 49, 51, 44, + 54, 53, 44, 45, 55, 48, 44, 52, 49, 44, 45, 57, 53, 44, 49, 48, 52, 44, + 45, 55, 53, 44, 45, 49, 49, 53, 44, 49, 48, 49, 44, 45, 57, 52, 44, 45, + 49, 50, 51, 44, 45, 51, 53, 44, 45, 50, 49, 44, 45, 52, 50, 44, 45, 51, + 48, 44, 45, 55, 49, 44, 45, 49, 49, 57, 44, 52, 52, 44, 49, 49, 49, 44, + 49, 48, 53, 44, 54, 54, 44, 45, 49, 50, 54, 44, 55, 50, 44, 45, 52, 48, + 44, 49, 50, 49, 44, 45, 50, 49, 44, 52, 50, 44, 45, 55, 56, 44, 49, 50, + 54, 44, 56, 49, 44, 45, 57, 52, 44, 55, 52, 44, 49, 49, 50, 44, 45, 56, + 54, 44, 51, 50, 44, 55, 54, 44, 49, 49, 55, 44, 45, 56, 44, 56, 54, 44, + 49, 48, 51, 44, 54, 50, 44, 49, 49, 55, 44, 54, 55, 44, 45, 56, 54, 44, + 45, 49, 48, 48, 44, 45, 49, 48, 57, 44, 45, 53, 52, 44, 45, 51, 49, 44, + 45, 56, 57, 44, 48, 93,125,125, + ] + + XCTAssertThrowsError(try JSONDecoder().decode( + FilePath.self, + from: Data(input) + )) + } + + func testInvalidSystemString() { + // _storage is a SystemString whose invariants are violated; it contains + // a non-terminating null byte. + let input: [UInt8] = [ + 123, 34, 95,115,116,111,114, 97,103,101, 34, 58,123, 34,110,117,108,108, + 84,101,114,109,105,110, 97,116,101,100, 83,116,111,114, 97,103,101, 34, + 58, 91, 49, 49, 49, 44, 48, 44, 45, 49, 54, 44, 57, 49, 44, 52, 54, 44, + 45, 49, 48, 50, 44, 49, 49, 53, 44, 45, 50, 49, 44, 45, 49, 49, 56, 44, + 52, 57, 44, 57, 50, 44, 45, 49, 48, 44, 53, 56, 44, 45, 55, 48, 44, 57, + 55, 44, 56, 44, 57, 57, 44, 48, 93,125, 125 + ] + + XCTAssertThrowsError(try JSONDecoder().decode( + FilePath.self, + from: Data(input) + )) + } + + func testInvalidExample() { + // Another misformed example from Johannes that violates FilePath's + // invariants by virtue of not being normalized. + let input: [UInt8] = [ + 123, 34, 95,115,116,111,114, 97,103,101, 34, 58,123, 34,110,117,108,108, + 84,101,114,109,105,110, 97,116,101,100, 83,116,111,114, 97,103,101, 34, + 58, 91, 56, 55, 44, 50, 52, 44, 45, 49, 49, 53, 44, 45, 49, 57, 44, 49, + 50, 50, 44, 45, 54, 56, 44, 57, 49, 44, 45, 49, 48, 54, 44, 45, 49, 48, + 48, 44, 45, 49, 49, 52, 44, 53, 54, 44, 45, 54, 53, 44, 49, 49, 56, 44, + 45, 54, 48, 44, 54, 54, 44, 45, 52, 50, 44, 55, 55, 44, 45, 54, 44, 45, + 52, 50, 44, 45, 56, 56, 44, 52, 55, 44, 48, 93,125, 125 + ] + + XCTAssertThrowsError(try JSONDecoder().decode( + FilePath.self, + from: Data(input) + )) + } + + func testEmptyString() { + // FilePath with an empty (and hence not null-terminated) SystemString. + let input: [UInt8] = [ + 123, 34, 95,115,116,111,114, 97,103,101, 34, 58,123, 34,110,117,108,108, + 84,101,114,109,105,110, 97,116,101,100, 83,116,111,114, 97,103,101, 34, + 58, 91, 93,125,125 + ] + + XCTAssertThrowsError(try JSONDecoder().decode( + FilePath.self, + from: Data(input) + )) + } +} diff --git a/Tests/SystemTests/FilePathTests/FilePathExtras.swift b/Tests/SystemTests/FilePathTests/FilePathExtras.swift new file mode 100644 index 00000000..ff4b1b37 --- /dev/null +++ b/Tests/SystemTests/FilePathTests/FilePathExtras.swift @@ -0,0 +1,107 @@ + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +// Why can't I write this extension on `FilePath.ComponentView.SubSequence`? +@available(System 0.0.2, *) +extension Slice where Base == FilePath.ComponentView { + internal var _storageSlice: SystemString.SubSequence { + base._path._storage[self.startIndex._storage ..< self.endIndex._storage] + } +} + + +// Proposed API that didn't make the cut, but we stil want to keep our testing for +@available(System 0.0.2, *) +extension FilePath { + /// Returns `self` relative to `base`. + /// This does not cosult the file system or resolve symlinks. + /// + /// Returns `nil` if `self.root != base.root`. + /// + /// On Windows, if any component of either path could be interpreted as the root of + /// a traditional DOS path (e.g. a directory named `C:`), returns `nil`. + /// + /// Example: + /// + /// let path: FilePath = "/usr/local/bin" + /// path.lexicallyRelative(toBase: "/usr/local") == "bin" + /// path.lexicallyRelative(toBase: "/usr/local/bin/ls") == ".." + /// path.lexicallyRelative(toBase: "/tmp/foo.txt") == "../../usr/local/bin" + /// path.lexicallyRelative(toBase: "local/bin") == nil + internal func lexicallyRelative(toBase base: FilePath) -> FilePath? { + guard root == base.root else { return nil } + + // FIXME: On Windows, return nil if any component looks like a root + + let (tail, baseTail) = _dropCommonPrefix(components, base.components) + + var prefix = SystemString() + for _ in 0.. Bool { + guard !other.isEmpty else { return true } + guard !isEmpty else { return false } + + let (selfLex, otherLex) = + (self.lexicallyNormalized(), other.lexicallyNormalized()) + if otherLex.isAbsolute { return selfLex.starts(with: otherLex) } + + // FIXME: Windows semantics with relative roots? + + // TODO: better than this naive algorithm + var slice = selfLex.components[...] + while !slice.isEmpty { + if slice.starts(with: otherLex.components) { return true } + slice = slice.dropFirst() + } + return false + } +} + +extension Collection where Element: Equatable, SubSequence == Slice { + // Mock up RangeSet functionality until it's real + func indices(where p: (Element) throws -> Bool) rethrows -> [Range] { + var result = Array>() + guard !isEmpty else { return result } + + var i = startIndex + while i != endIndex { + let next = index(after: i) + if try p(self[i]) { + result.append(i..]) { + guard !subranges.isEmpty else { return } + + var result = Self() + var idx = startIndex + for range in subranges { + result.append(contentsOf: self[idx.. ParsingTestCase { + ParsingTestCase( + isWindows: false, + pathStr: path, normalized: normalized, + file: file, line: line) + } + + static func windows( + _ path: String, normalized: String, + file: StaticString = #file, line: UInt = #line + ) -> ParsingTestCase { + ParsingTestCase( + isWindows: true, + pathStr: path, normalized: normalized, + file: file, line: line) + } +} + +extension ParsingTestCase { + func runAllTests() { + withWindowsPaths(enabled: isWindows) { + let path = FilePath(pathStr) + expectEqual(normalized, path.description) + } + } +} + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@available(System 0.0.2, *) +final class FilePathParsingTest: XCTestCase { + func testNormalization() { + let unixPaths: Array = [ + .unix("/", normalized: "/"), + .unix("", normalized: ""), + .unix("//", normalized: "/"), + .unix("///", normalized: "/"), + .unix("/foo/bar/", normalized: "/foo/bar"), + .unix("foo//bar", normalized: "foo/bar"), + .unix("//foo/bar//baz/", normalized: "/foo/bar/baz"), + .unix("/foo/bar/baz//", normalized: "/foo/bar/baz"), + .unix("/foo/bar/baz///", normalized: "/foo/bar/baz"), + ] + + let windowsPaths: Array = [ + .windows(#"C:\\folder\file\"#, normalized: #"C:\folder\file"#), + .windows(#"C:folder\\\file\\\"#, normalized: #"C:folder\file"#), + .windows(#"C:/foo//bar/"#, normalized: #"C:\foo\bar"#), + + .windows(#"\\server\share\"#, normalized: #"\\server\share\"#), + .windows(#"//server/share/"#, normalized: #"\\server\share\"#), + .windows(#"\\?\UNC/server\share\"#, normalized: #"\\?\UNC\server\share\"#), + + .windows(#"\\.\C:\"#, normalized: #"\\.\C:\"#), + .windows(#"C:\"#, normalized: #"C:\"#), + .windows(#"\"#, normalized: #"\"#), + ] + + for test in unixPaths { + test.runAllTests() + } + for test in windowsPaths { + test.runAllTests() + } + } +} +#endif // ENABLE_MOCKING diff --git a/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift b/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift new file mode 100644 index 00000000..0fdfbc27 --- /dev/null +++ b/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift @@ -0,0 +1,1242 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 ENABLE_MOCKING +import XCTest + +#if SYSTEM_PACKAGE +import SystemPackage +#else +import System +#endif + +private struct SyntaxTestCase: TestCase { + // Whether we want the path to be constructed and syntactically + // manipulated as though it were a Windows path + let isWindows: Bool + + // We defer forming the path until `runAllTests()` executes, + // so that we can switch between unix and windows behavior. + let pathStr: String + + let normalized: String + + let absolute: Bool + + let root: String? + let relative: String + + let dirname: String + let basename: String? + + let stem: String? + let `extension`: String? + + let components: [String] + + let lexicallyNormalized: String + + var file: StaticString + var line: UInt +} + +extension SyntaxTestCase { + // Convenience constructor which can substitute sensible defaults + private static func testCase( + isWindows: Bool, + _ path: String, + + // Nil `normalized` means use `path` + normalized: String?, + + // Nil means use the precense of a root + absolute: Bool?, + + // Nil `root` means no root. Nil `relative` means use `normalized` + root: String?, relative: String?, + + // Nil `dirname` requires nil `basename` and means use `normalized` + dirname: String?, basename: String?, + + // `nil` stem means use `basename` + stem: String?, extension: String?, + + components: [String], + + // Nil `lexicallyNormalized` means use `normalized` + lexicallyNormalized: String?, + + file: StaticString, line: UInt + ) -> SyntaxTestCase { + if dirname == nil { + assert(basename == nil ) + } + + let normalized = normalized ?? path + let lexicallyNormalized = lexicallyNormalized ?? normalized + let absolute = absolute ?? (root != nil) + let relative = relative ?? normalized + let dirname = dirname ?? (normalized) + let stem = stem ?? basename + return SyntaxTestCase( + isWindows: isWindows, + pathStr: path, + normalized: normalized, + absolute: absolute, + root: root, relative: relative, + dirname: dirname, basename: basename, + stem: stem, extension: `extension`, + components: components, + lexicallyNormalized: lexicallyNormalized, + file: file, line: line) + } + + // Conveience constructor for unix path test cases + static func unix( + _ path: String, + normalized: String? = nil, + root: String? = nil, relative: String? = nil, + dirname: String? = nil, basename: String? = nil, + stem: String? = nil, extension: String? = nil, + components: [String], + lexicallyNormalized: String? = nil, + file: StaticString = #file, line: UInt = #line + ) -> SyntaxTestCase { + .testCase( + isWindows: false, + path, + normalized: normalized, + absolute: nil, + root: root, relative: relative, + dirname: dirname, basename: basename, + stem: stem, extension: `extension`, + components: components, + lexicallyNormalized: lexicallyNormalized, + file: file, line: line) + } + + // Conveience constructor for unix path test cases + static func windows( + _ path: String, + normalized: String? = nil, + absolute: Bool, + root: String? = nil, relative: String? = nil, + dirname: String? = nil, basename: String? = nil, + stem: String? = nil, extension: String? = nil, + components: [String], + lexicallyNormalized: String? = nil, + file: StaticString = #file, line: UInt = #line + ) -> SyntaxTestCase { + .testCase( + isWindows: true, + path, + normalized: normalized, + absolute: absolute, + root: root, relative: relative, + dirname: dirname, basename: basename, + stem: stem, extension: `extension`, + components: components, + lexicallyNormalized: lexicallyNormalized, + file: file, line: line) + } +} + +@available(System 0.0.2, *) +extension SyntaxTestCase { + func testComponents(_ path: FilePath, expected: [String]) { + let expectedComponents = expected.map { FilePath.Component($0)! } + + expectEqualSequence(expectedComponents, Array(path.components), + "expected components") + expectEqualSequence(expectedComponents, Array(path.removingRoot().components), + "expected components") + + expectEqualSequence(expectedComponents, path.components) + expectEqual(expectedComponents.first, path.components.first) + expectEqual(expectedComponents.last, path.components.last) + + expectEqual(path, FilePath(root: path.root, expectedComponents), "init(root:components)") + expectEqual(path, FilePath(root: path.root, path.components), "init(root:components)") + expectEqual(path, FilePath( + root: path.root, path.components[...]), + "init(_ components: Slice)") + + let reversedPathComponents = path.components.reversed() + expectEqual(expectedComponents.count, reversedPathComponents.count) + expectEqualSequence(expectedComponents.reversed(), reversedPathComponents, "reversed") + + expectEqualSequence( + expectedComponents, reversedPathComponents.reversed(), "doubly reversed") + + expectEqualSequence(path.removingRoot().components, path.components, + "relativeComponents") + + let doublyReversed = FilePath( + root: nil, path.removingRoot().components.reversed() + ).components.reversed() + expectEqualSequence(path.removingRoot().components, doublyReversed, + "relative path doubly reversed") + + expectTrue(path.starts(with: path.removingLastComponent()), + "starts(with: dirname)") + expectEqual(path.removingLastComponent(), FilePath( + root: path.root, path.components.dropLast()), + "ComponentView.dirname") + + var prefixComps = expectedComponents + var prefixBasenamePath = path + var prefixPopLastPath = path + var compView = path.components[...] + var prefixDirname = path.removingLastComponent() + while !prefixComps.isEmpty { + expectTrue(path.starts(with: FilePath(root: path.root, prefixComps)), "startswith") + expectTrue(path.starts(with: prefixBasenamePath), "startswith") + expectTrue(path.starts(with: prefixPopLastPath), "startswith") + expectEqual(prefixBasenamePath, prefixPopLastPath, "popLast/basename") + expectEqual(prefixBasenamePath, FilePath(root: path.root, compView), + "popLast/basename") + prefixComps.removeLast() + prefixBasenamePath = prefixBasenamePath.removingLastComponent() + prefixPopLastPath.removeLastComponent() + compView = compView.dropLast() + + expectEqual(prefixBasenamePath, prefixDirname, "prefix dirname") + prefixDirname = prefixDirname.removingLastComponent() + } + var suffixComps = expectedComponents + compView = path.components[...] + while !suffixComps.isEmpty { + expectTrue(path.ends(with: FilePath(root: nil, suffixComps)), "endswith") + expectEqual(FilePath(root: nil, compView), FilePath(root: nil, suffixComps)) + suffixComps.removeFirst() + compView = compView.dropFirst() + } + + // FIXME: If we add operator `/` back, uncomment this + #if false + let slashPath = _path.components.reduce("", /) + let pushPath: FilePath = _path.components.reduce( + into: "", { $0.pushLast($1) }) + + expectEqual(_path, slashPath, "`/`") + expectEqual(_path, pushPath, "pushLast") + #endif + } + + + func runAllTests() { + // Assert we were set up correctly if non-nil + func assertNonEmpty(_ c: C?) { + assert(c == nil || !c!.isEmpty) + } + assertNonEmpty(root) + assertNonEmpty(basename) + assertNonEmpty(stem) + + withWindowsPaths(enabled: isWindows) { + let path = FilePath(pathStr) + + expectTrue((path == "") == path.isEmpty, "isEmpty") + + expectEqual(normalized, path.description, "normalized") + + var copy = path + copy.lexicallyNormalize() + expectEqual(copy == path, path.isLexicallyNormal, "isLexicallyNormal") + expectEqual(lexicallyNormalized, copy.description, "lexically normalized") + expectEqual(copy, path.lexicallyNormalized(), "lexicallyNormal") + + expectEqual(absolute, path.isAbsolute, "absolute") + expectEqual(!absolute, path.isRelative, "!absolute") + + expectEqual(root, path.root?.description, "root") + expectEqual(relative, path.removingRoot().description, "relative") + + if path.isRelative { + if path.root == nil { + expectEqual(path, path.removingRoot(), "relative idempotent") + } else { + expectTrue(isWindows) + expectFalse(path == path.removingRoot()) + var relPathCopy = path.removingRoot() + relPathCopy.root = path.root + expectEqual(path, relPathCopy) + + // TODO: Windows root analysis tests + } + } else { + var pathCopy = path + pathCopy.root = nil + expectEqual(pathCopy, path.removingRoot(), "set nil root") + expectEqual(relative, path.removingRoot().description, "set root to nil") + pathCopy.root = path.root + expectTrue(pathCopy.isAbsolute) + expectEqual(path, pathCopy) + expectTrue(path.root != nil) + } + + if let root = path.root { + var pathCopy = path + pathCopy.components.removeAll() + expectEqual(FilePath(root: root), pathCopy, "set empty relative") + } else { + var pathCopy = path + pathCopy.components.removeAll() + expectTrue(pathCopy.isEmpty, "set empty relative") + } + + expectEqual(dirname, path.removingLastComponent().description, "dirname") + expectEqual(basename, path.lastComponent?.description, "basename") + + do { + var path = path + var pathCopy = path + while !path.removingRoot().isEmpty { + pathCopy = pathCopy.removingLastComponent() + path.removeLastComponent() + expectEqual(path, pathCopy) + } + } + + expectEqual(stem, path.stem, "stem") + expectEqual(`extension`, path.extension, "extension") + + if let base = path.lastComponent { + expectEqual(path.stem, base.stem) + expectEqual(path.extension, base.extension) + } + + var pathCopy = path + while pathCopy.extension != nil { + var name = pathCopy.lastComponent!.description + name.removeSubrange(name.lastIndex(of: ".")!...) + pathCopy.extension = nil + expectEqual(name, pathCopy.lastComponent!.description, "set nil extension (2)") + } + expectTrue(pathCopy.extension == nil, "set nil extension") + + pathCopy = path + pathCopy.removeAll(keepingCapacity: true) + expectTrue(pathCopy.isEmpty) + + testComponents(path, expected: self.components) + } + } +} + +private struct WindowsRootTestCase: TestCase { + // We defer forming the path until `runAllTests()` executes, + // so that we can switch between unix and windows behavior. + let rootStr: String + + let expected: String + + let absolute: Bool + + var file: StaticString + var line: UInt +} + +@available(System 0.0.2, *) +extension WindowsRootTestCase { + func runAllTests() { + withWindowsPaths(enabled: true) { + let path = FilePath(rootStr) + expectEqual(expected, path.string) + expectNotNil(path.root) + expectEqual(path, FilePath(root: path.root ?? "")) + expectTrue(path.components.isEmpty) + expectTrue(path.removingRoot().isEmpty) + expectTrue(path.isLexicallyNormal) + } + } +} + +@available(System 0.0.2, *) +final class FilePathSyntaxTest: XCTestCase { + func testPathSyntax() { + let unixPaths: Array = [ + .unix("", components: []), + + .unix( + "/", + root: "/", relative: "", + components: [] + ), + + .unix( + "/..", + root: "/", relative: "..", + dirname: "/", basename: "..", + components: [".."], + lexicallyNormalized: "/" + ), + + .unix( + "/.", + root: "/", relative: ".", + dirname: "/", basename: ".", + components: ["."], + lexicallyNormalized: "/" + ), + + .unix( + "/../.", + root: "/", relative: "../.", + dirname: "/..", basename: ".", + components: ["..", "."], + lexicallyNormalized: "/" + ), + + .unix( + ".", + dirname: "", basename: ".", + components: ["."], + lexicallyNormalized: "" + ), + + .unix( + "..", + dirname: "", basename: "..", + components: [".."], + lexicallyNormalized: ".." + ), + + .unix( + "./..", + dirname: ".", basename: "..", + components: [".", ".."], + lexicallyNormalized: ".." + ), + + .unix( + "../.", + dirname: "..", basename: ".", + components: ["..", "."], + lexicallyNormalized: ".." + ), + + .unix( + "../..", + dirname: "..", basename: "..", + components: ["..", ".."], + lexicallyNormalized: "../.." + ), + + .unix( + "a/../..", + dirname: "a/..", basename: "..", + components: ["a", "..", ".."], + lexicallyNormalized: ".." + ), + + .unix( + "a/.././.././../b", + dirname: "a/.././.././..", basename: "b", + components: ["a", "..", ".", "..", ".", "..", "b"], + lexicallyNormalized: "../../b" + ), + + .unix( + "/a/.././.././../b", + root: "/", relative: "a/.././.././../b", + dirname: "/a/.././.././..", basename: "b", + components: ["a", "..", ".", "..", ".", "..", "b"], + lexicallyNormalized: "/b" + ), + + .unix( + "./.", + dirname: ".", basename: ".", + components: [".", "."], + lexicallyNormalized: "" + ), + + .unix( + "foo.txt", + dirname: "", basename: "foo.txt", + stem: "foo", extension: "txt", + components: ["foo.txt"] + ), + + .unix( + "a/foo/bar/../..", + dirname: "a/foo/bar/..", basename: "..", + components: ["a", "foo", "bar", "..", ".."], + lexicallyNormalized: "a" + ), + + .unix( + "a/./foo/bar/.././../.", + dirname: "a/./foo/bar/.././..", basename: ".", + components: ["a", ".", "foo", "bar", "..", ".", "..", "."], + lexicallyNormalized: "a" + ), + + .unix( + "a/../b", + dirname: "a/..", basename: "b", + components: ["a", "..", "b"], + lexicallyNormalized: "b" + ), + + .unix( + "/a/../b/../c/../../d", + root: "/", relative: "a/../b/../c/../../d", + dirname: "/a/../b/../c/../..", basename: "d", + components: ["a", "..", "b", "..", "c", "..", "..", "d"], + lexicallyNormalized: "/d" + ), + + .unix( + "/usr/bin/ls", + root: "/", relative: "usr/bin/ls", + dirname: "/usr/bin", basename: "ls", + components: ["usr", "bin", "ls"] + ), + + .unix( + "bin/ls", + dirname: "bin", basename: "ls", + components: ["bin", "ls"] + ), + + .unix( + "~/bar.app", + dirname: "~", basename: "bar.app", + stem: "bar", extension: "app", + components: ["~", "bar.app"] + ), + + .unix( + "~/bar.app.bak/", + normalized: "~/bar.app.bak", + dirname: "~", basename: "bar.app.bak", + stem: "bar.app", extension: "bak", + components: ["~", "bar.app.bak"] + ), + + .unix( + "/tmp/.", + root: "/", relative: "tmp/.", + dirname: "/tmp", basename: ".", + components: ["tmp", "."], + lexicallyNormalized: "/tmp" + ), + + .unix( + "/tmp/..", + root: "/", relative: "tmp/..", + dirname: "/tmp", basename: "..", + components: ["tmp", ".."], + lexicallyNormalized: "/" + ), + + .unix( + "/tmp/../", + normalized: "/tmp/..", + root: "/", relative: "tmp/..", + dirname: "/tmp", basename: "..", + components: ["tmp", ".."], + lexicallyNormalized: "/" + ), + + .unix( + "/tmp/./a/../b", + root: "/", relative: "tmp/./a/../b", + dirname: "/tmp/./a/..", basename: "b", + components: ["tmp", ".", "a", "..", "b"], + lexicallyNormalized: "/tmp/b" + ), + + .unix( + "/tmp/.hidden", + root: "/", relative: "tmp/.hidden", + dirname: "/tmp", basename: ".hidden", + components: ["tmp", ".hidden"] + ), + + .unix( + "/tmp/.hidden.", + root: "/", relative: "tmp/.hidden.", + dirname: "/tmp", basename: ".hidden.", + stem: ".hidden", extension: "", + components: ["tmp", ".hidden."] + ), + + .unix( + "/tmp/.hidden.o", + root: "/", relative: "tmp/.hidden.o", + dirname: "/tmp", basename: ".hidden.o", + stem: ".hidden", extension: "o", + components: ["tmp", ".hidden.o"] + ), + + .unix( + "/tmp/.hidden.o.", + root: "/", relative: "tmp/.hidden.o.", + dirname: "/tmp", basename: ".hidden.o.", + stem: ".hidden.o", extension: "", + components: ["tmp", ".hidden.o."] + ), + + // Backslash is not a separator, nor a root + .unix( + #"\bin\.\ls"#, + dirname: "", basename: #"\bin\.\ls"#, + stem: #"\bin\"#, extension: #"\ls"#, + components: [#"\bin\.\ls"#] + ), + ] + + let windowsPaths: Array = [ + .windows(#""#, absolute: false, components: []), + + .windows( + #"C"#, + absolute: false, + dirname: "", basename: "C", + components: ["C"] + ), + + .windows( + #"C:"#, + absolute: false, + root: #"C:"#, relative: #""#, + components: [] + ), + + .windows( + #"C:\"#, + absolute: true, + root: #"C:\"#, relative: #""#, + components: [] + ), + + .windows( + #"C:\foo\bar.exe"#, + absolute: true, + root: #"C:\"#, relative: #"foo\bar.exe"#, + dirname: #"C:\foo"#, basename: "bar.exe", + stem: "bar", extension: "exe", + components: ["foo", "bar.exe"] + ), + + .windows( + #"C:foo\bar"#, + absolute: false, + root: #"C:"#, relative: #"foo\bar"#, + dirname: #"C:foo"#, basename: "bar", + components: ["foo", "bar"] + ), + + .windows( + #"C:foo\bar\..\.."#, + absolute: false, + root: #"C:"#, relative: #"foo\bar\..\.."#, + dirname: #"C:foo\bar\.."#, basename: "..", + components: ["foo", "bar", "..", ".."], + lexicallyNormalized: "C:" + ), + + .windows( + #"C:foo\bar\..\..\.."#, + absolute: false, + root: #"C:"#, relative: #"foo\bar\..\..\.."#, + dirname: #"C:foo\bar\..\.."#, basename: "..", + components: ["foo", "bar", "..", "..", ".."], + lexicallyNormalized: "C:" + ), + + .windows( + #"\foo\bar.exe"#, + absolute: false, + root: #"\"#, relative: #"foo\bar.exe"#, + dirname: #"\foo"#, basename: "bar.exe", + stem: "bar", extension: "exe", + components: ["foo", "bar.exe"] + ), + + .windows( + #"foo\bar.exe"#, + absolute: false, + dirname: #"foo"#, basename: "bar.exe", + stem: "bar", extension: "exe", + components: ["foo", "bar.exe"] + ), + + .windows( + #"\\?\device\"#, + absolute: true, + root: #"\\?\device\"#, relative: "", + components: [] + ), + + .windows( + #"\\?\device\folder\file.exe"#, + absolute: true, + root: #"\\?\device\"#, relative: #"folder\file.exe"#, + dirname: #"\\?\device\folder"#, basename: "file.exe", + stem: "file", extension: "exe", + components: ["folder", "file.exe"] + ), + + .windows( + #"\\?\UNC\server\share\"#, + absolute: true, + root: #"\\?\UNC\server\share\"#, relative: #""#, + components: [] + ), + + .windows( + #"\\?\UNC\server\share\folder\file.txt"#, + absolute: true, + root: #"\\?\UNC\server\share\"#, relative: #"folder\file.txt"#, + dirname: #"\\?\UNC\server\share\folder"#, basename: "file.txt", + stem: "file", extension: "txt", + components: ["folder", "file.txt"] + ), + + .windows( + #"\\server\share\"#, + absolute: true, + root: #"\\server\share\"#, relative: "", + components: [] + ), + + .windows( + #"\\server\share\folder\file.txt"#, + absolute: true, + root: #"\\server\share\"#, relative: #"folder\file.txt"#, + dirname: #"\\server\share\folder"#, basename: "file.txt", + stem: "file", extension: "txt", + components: ["folder", "file.txt"] + ), + + .windows( + #"\\server\share\folder\file.txt\.."#, + absolute: true, + root: #"\\server\share\"#, relative: #"folder\file.txt\.."#, + dirname: #"\\server\share\folder\file.txt"#, basename: "..", + components: ["folder", "file.txt", ".."], + lexicallyNormalized: #"\\server\share\folder"# + ), + + .windows( + #"\\server\share\folder\file.txt\..\.."#, + absolute: true, + root: #"\\server\share\"#, relative: #"folder\file.txt\..\.."#, + dirname: #"\\server\share\folder\file.txt\.."#, basename: "..", + components: ["folder", "file.txt", "..", ".."], + lexicallyNormalized: #"\\server\share\"# + ), + + .windows( + #"\\server\share\folder\file.txt\..\..\..\.."#, + absolute: true, + root: #"\\server\share\"#, relative: #"folder\file.txt\..\..\..\.."#, + dirname: #"\\server\share\folder\file.txt\..\..\.."#, basename: "..", + components: ["folder", "file.txt", "..", "..", "..", ".."], + lexicallyNormalized: #"\\server\share\"# + ), + + // Actually a rooted relative path + .windows( + #"\server\share\folder\file.txt\..\..\.."#, + absolute: false, + root: #"\"#, relative: #"server\share\folder\file.txt\..\..\.."#, + dirname: #"\server\share\folder\file.txt\..\.."#, basename: "..", + components: ["server", "share", "folder", "file.txt", "..", "..", ".."], + lexicallyNormalized: #"\server"# + ), + + .windows( + #"\\?\Volume{12345678-abcd-1111-2222-123445789abc}\folder\file"#, + absolute: true, + root: #"\\?\Volume{12345678-abcd-1111-2222-123445789abc}\"#, + relative: #"folder\file"#, + dirname: #"\\?\Volume{12345678-abcd-1111-2222-123445789abc}\folder"#, + basename: "file", + components: ["folder", "file"] + ) + + // TODO: partially-formed Windows roots, we should fully form them... + ] + + for test in unixPaths { + test.runAllTests() + } + for test in windowsPaths { + test.runAllTests() + } + } + + + func testPrefixSuffix() { + let startswith: Array<(String, String)> = [ + ("/usr/bin/ls", "/"), + ("/usr/bin/ls", "/usr"), + ("/usr/bin/ls", "/usr/bin"), + ("/usr/bin/ls", "/usr/bin/ls"), + ("/usr/bin/ls", "/usr/bin/ls//"), + ("/usr/bin/ls", ""), + ] + + let noStartswith: Array<(String, String)> = [ + ("/usr/bin/ls", "/u"), + ("/usr/bin/ls", "/us"), + ("/usr/bin/ls", "/usr/bi"), + ("/usr/bin/ls", "usr/bin/ls"), + ("/usr/bin/ls", "usr/"), + ("/usr/bin/ls", "ls"), + ] + + for (path, pre) in startswith { + XCTAssert(FilePath(path).starts(with: FilePath(pre))) + } + for (path, pre) in noStartswith { + XCTAssertFalse(FilePath(path).starts(with: FilePath(pre))) + } + + let endswith: Array<(String, String)> = [ + ("/usr/bin/ls", "ls"), + ("/usr/bin/ls", "bin/ls"), + ("/usr/bin/ls", "usr/bin/ls"), + ("/usr/bin/ls", "/usr/bin/ls"), + ("/usr/bin/ls", "/usr/bin/ls///"), + ("/usr/bin/ls", ""), + ] + + let noEndswith: Array<(String, String)> = [ + ("/usr/bin/ls", "/ls"), + ("/usr/bin/ls", "/bin/ls"), + ("/usr/bin/ls", "/usr/bin"), + ("/usr/bin/ls", "foo"), + ] + + for (path, suf) in endswith { + XCTAssert(FilePath(path).ends(with: FilePath(suf))) + } + for (path, suf) in noEndswith { + XCTAssertFalse(FilePath(path).ends(with: FilePath(suf))) + } + } + + func testLexicallyRelative() { + let path: FilePath = "/usr/local/bin" + XCTAssert(path.lexicallyRelative(toBase: "/usr/local") == "bin") + XCTAssert(path.lexicallyRelative(toBase: "/usr/local/bin/ls") == "..") + XCTAssert(path.lexicallyRelative(toBase: "/tmp/foo.txt") == "../../usr/local/bin") + XCTAssert(path.lexicallyRelative(toBase: "local/bin") == nil) + + let rel = FilePath(root: nil, path.components) + XCTAssert(rel.lexicallyRelative(toBase: "/usr/local") == nil) + XCTAssert(rel.lexicallyRelative(toBase: "usr/local") == "bin") + XCTAssert(rel.lexicallyRelative(toBase: "usr/local/bin/ls") == "..") + XCTAssert(rel.lexicallyRelative(toBase: "tmp/foo.txt") == "../../usr/local/bin") + XCTAssert(rel.lexicallyRelative(toBase: "local/bin") == "../../usr/local/bin") + + // TODO: Test Windows path with root pushed + } + + func testAdHocMutations() { + var path: FilePath = "/usr/local/bin" + + func expect( + _ s: String, + _ file: StaticString = #filePath, + _ line: UInt = #line + ) { + if path == FilePath(s) { return } + + defer { print("expected: \(s), actual: \(path)") } + XCTAssert(false, file: file, line: line) + } + + // Run `body`, restoring `path` afterwards + func restoreAfter( + body: () -> () + ) { + let copy = path + defer { path = copy } + body() + } + + restoreAfter { + path.root = nil + expect("usr/local/bin") + path.components = FilePath("ls").components + expect("ls") + } + + restoreAfter { + path.components = FilePath("/bin/ls").components + expect("/bin/ls") + path.components.removeAll() + expect("/") + } + + restoreAfter { + path = path.removingLastComponent().appending("lib") + expect("/usr/local/lib") + path = path.removingLastComponent() + expect("/usr/local") + path = path.removingLastComponent().appending("bin") + expect("/usr/bin") + } + + restoreAfter { + path = FilePath("~").appending(path.lastComponent!) + expect("~/bin") + path = FilePath("").appending(path.lastComponent!) + expect("bin") + path = FilePath("").appending(path.lastComponent!) + expect("bin") + path = FilePath("/usr/local").appending(path.lastComponent!) + expect("/usr/local/bin") + } + + restoreAfter { + path.removeLastComponent() + expect("/usr/local") + path.removeLastComponent() + expect("/usr") + path.removeLastComponent() + expect("/") + path.removeLastComponent() + expect("/") + + path.removeAll() + expect("") + + path.append("tmp") + expect("tmp") + path.append("cat") + expect("tmp/cat") + path.push("/") + expect("/") + + path.append(".") + expect("/.") + XCTAssert(path.components.last!.kind == .currentDirectory) + path.lexicallyNormalize() + expect("/") + + path.append("..") + expect("/..") + XCTAssert(path.components.last!.kind == .parentDirectory) + path.lexicallyNormalize() + expect("/") + + path.append("foo") + path.append("..") + expect("/foo/..") + path.lexicallyNormalize() + expect("/") + } + + restoreAfter { + path.append("ls") + expect("/usr/local/bin/ls") + path.extension = "exe" + expect("/usr/local/bin/ls.exe") + path.extension = "txt" + expect("/usr/local/bin/ls.txt") + + path.extension = nil + expect("/usr/local/bin/ls") + + path.extension = "" + expect("/usr/local/bin/ls.") + XCTAssert(path.extension == "") + path.extension = "txt" + expect("/usr/local/bin/ls.txt") + } + + restoreAfter { + path.append("..") + expect("/usr/local/bin/..") + XCTAssert(path.components.last!.kind == .parentDirectory) + path.extension = "txt" + expect("/usr/local/bin/..") + XCTAssert(path.components.last!.kind == .parentDirectory) + path.removeAll() + expect("") + path.extension = "txt" + expect("") + path.append("/") + expect("/") + path.extension = "txt" + expect("/") + } + + restoreAfter { + XCTAssert(!path.removePrefix("/usr/bin")) + expect("/usr/local/bin") + + XCTAssert(!path.removePrefix("/us")) + expect("/usr/local/bin") + + XCTAssert(path.removePrefix("/usr/local")) + expect("bin") + + XCTAssert(path.removePrefix("bin")) + expect("") + } + + restoreAfter { + path.append("utils/widget/") + expect("/usr/local/bin/utils/widget") + path.append("/bin///ls") + expect("/usr/local/bin/utils/widget/bin/ls") + path.push("/bin/ls") + expect("/bin/ls") + path.append("/") + expect("/bin/ls") + path.push("/") + expect("/") + + path.append("tmp") + expect("/tmp") + path.append("foo/bar") + expect("/tmp/foo/bar") + XCTAssert(!path.isEmpty) + + path.append(FilePath.Component("baz")) + expect("/tmp/foo/bar/baz") + path.append("/") + expect("/tmp/foo/bar/baz") + path.removeAll() + expect("") + XCTAssert(path.isEmpty) + path.append("") + expect("") + + path.append("/bar/baz") + expect("/bar/baz") + path.removeAll() + expect("") + path.append(FilePath.Component("usr")) + expect("usr") + path.push("/bin/ls") + expect("/bin/ls") + path.removeAll() + expect("") + + path.append("bar/baz") + expect("bar/baz") + + path.append(["a", "b", "c"]) + expect("bar/baz/a/b/c") + + path.removeAll() + expect("") + path.append(["a", "b", "c"]) + expect("a/b/c") + } + + restoreAfter { + expect("/usr/local/bin") + path.push("bar/baz") + expect("/usr/local/bin/bar/baz") + path.push("/") + expect("/") + path.push("tmp") + expect("/tmp") + path.push("/dev/null") + expect("/dev/null") + } + + restoreAfter { + let same = path.string + path.reserveCapacity(0) + expect(same) + path.reserveCapacity(1000) + expect(same) + } + + restoreAfter { + XCTAssert(path.lexicallyContains("usr")) + XCTAssert(path.lexicallyContains("/usr")) + XCTAssert(path.lexicallyContains("local/bin")) +#if !os(Windows) + // On Windows, this is a relative path and is still contained + XCTAssert(!path.lexicallyContains("/local/bin")) +#endif + path.append("..") + XCTAssert(!path.lexicallyContains("local/bin")) + XCTAssert(path.lexicallyContains("local/bin/..")) + expect("/usr/local/bin/..") + XCTAssert(path.lexicallyContains("usr/local")) + XCTAssert(path.lexicallyContains("usr/local/.")) + } + + restoreAfter { + XCTAssert(path.lexicallyResolving("ls") == "/usr/local/bin/ls") + XCTAssert(path.lexicallyResolving("/ls") == "/usr/local/bin/ls") + XCTAssert(path.lexicallyResolving("../bin/ls") == nil) + XCTAssert(path.lexicallyResolving("/../bin/ls") == nil) + + XCTAssert(path.lexicallyResolving("/../bin/../lib/target") == nil) + XCTAssert(path.lexicallyResolving("./ls/../../lib/target") == nil) + + let staticContent: FilePath = "/var/www/my-website/static" + let links: [FilePath] = + ["index.html", "/assets/main.css", "../../../../etc/passwd"] + let paths = links.map { staticContent.lexicallyResolving($0) } + XCTAssert(paths == [ + "/var/www/my-website/static/index.html", + "/var/www/my-website/static/assets/main.css", + nil]) + + } + + restoreAfter { + path = "/tmp" + let sub: FilePath = "foo/./bar/../baz/." + for comp in sub.components.filter({ $0.kind != .currentDirectory }) { + path.append(comp) + } + expect("/tmp/foo/bar/../baz") + } + + restoreAfter { + path = "/usr/bin" + let binIdx = path.components.firstIndex(of: "bin")! + path.components.insert("local", at: binIdx) + expect("/usr/local/bin") + } + + restoreAfter { + path = "/./home/./username/scripts/./tree" + let scriptIdx = path.components.lastIndex(of: "scripts")! + path.components.insert("bin", at: scriptIdx) + expect("/./home/./username/bin/scripts/./tree") + + path.components.removeAll { $0.kind == .currentDirectory } + expect("/home/username/bin/scripts/tree") + } + + restoreAfter { + path = "/usr/bin" + XCTAssert(path.removeLastComponent()) + expect("/usr") + XCTAssert(path.removeLastComponent()) + expect("/") + XCTAssertFalse(path.removeLastComponent()) + expect("/") + } + + restoreAfter { + path = "" + path.append("/var/www/website") + expect("/var/www/website") + path.append("static/assets") + expect("/var/www/website/static/assets") + path.append("/main.css") + expect("/var/www/website/static/assets/main.css") + + } + } + + func testFailableStringInitializers() { + let invalidComps: Array = [ + "", "/", "a/b", + ] + let invalidRoots: Array = [ + "", "a", "a/b", + ] + for c in invalidComps { + XCTAssertNil(FilePath.Component(c)) + } + for c in invalidRoots { + XCTAssertNil(FilePath.Root(c)) + } + + // Due to SE-0213, this is how you call he failable init explicitly, + // otherwise it will be considered a literal `as` cast. + XCTAssertNil(FilePath.Component.init("/")) + } + + func testPartialWindowsRoots() { + func partial( + _ str: String, + _ full: String, + absolute: Bool = true, + file: StaticString = #file, line: UInt = #line + ) -> WindowsRootTestCase { + WindowsRootTestCase( + rootStr: str, expected: full, absolute: absolute, + file: file, line: line) + } + func full( + _ str: String, absolute: Bool = true, + file: StaticString = #file, line: UInt = #line + ) -> WindowsRootTestCase { + partial(str, str, absolute: absolute, + file: file, line: line) + } + + // TODO: Some of these are kinda funky (like `\\` -> `\\\\`), but + // I'm not aware of a sane fixup behavior here, so we go + // with a lesser of insanes. + let partialRootTestCases: [WindowsRootTestCase] = [ + // Full roots + full(#"\"#, absolute: false), + full(#"C:"#, absolute: false), + full(#"C:\"#), + + // Full UNCs (with omitted fields) + full(#"\\server\share\"#), + full(#"\\server\\"#), + full(#"\\\share\"#), + full(#"\\\\"#), + + // Full device UNCs (with omitted fields) + full(#"\\.\UNC\server\share\"#), + full(#"\\.\UNC\server\\"#), + full(#"\\.\UNC\\share\"#), + full(#"\\.\UNC\\\"#), + + // Full device (with omitted fields) + full(#"\\.\volume\"#), + full(#"\\.\\"#), + + // Partial UNCs + partial(#"\\server\share"#, #"\\server\share\"#), + partial(#"\\server\"#, #"\\server\\"#), + partial(#"\\server"#, #"\\server\\"#), + partial(#"\\\\"#, #"\\\\"#), + partial(#"\\\"#, #"\\\\"#), + partial(#"\\"#, #"\\\\"#), + + // Partial device UNCs + partial(#"\\.\UNC\server\share"#, #"\\.\UNC\server\share\"#), + partial(#"\\.\UNC\server\"#, #"\\.\UNC\server\\"#), + partial(#"\\.\UNC\server"#, #"\\.\UNC\server\\"#), + partial(#"\\.\UNC\\\"#, #"\\.\UNC\\\"#), + partial(#"\\.\UNC\\"#, #"\\.\UNC\\\"#), + partial(#"\\.\UNC\"#, #"\\.\UNC\\\"#), + partial(#"\\.\UNC"#, #"\\.\UNC\\\"#), + + // Partial device + partial(#"\\.\volume"#, #"\\.\volume\"#), + partial(#"\\.\"#, #"\\.\\"#), + partial(#"\\."#, #"\\.\\"#), + ] + + for partialRootTest in partialRootTestCases { + partialRootTest.runAllTests() + } + + + } + +} +#endif // ENABLE_MOCKING diff --git a/Tests/SystemTests/FilePathTests/FilePathTempTest.swift b/Tests/SystemTests/FilePathTests/FilePathTempTest.swift new file mode 100644 index 00000000..30d58261 --- /dev/null +++ b/Tests/SystemTests/FilePathTests/FilePathTempTest.swift @@ -0,0 +1,58 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2024 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 +*/ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +final class TemporaryPathTest: XCTestCase { + #if SYSTEM_PACKAGE_DARWIN + func testNotInSlashTmp() throws { + try withTemporaryFilePath(basename: "NotInSlashTmp") { path in + // We shouldn't be using "/tmp" on Darwin + XCTAssertNotEqual(path.components.first!, "tmp") + } + } + #endif + + func testUnique() throws { + try withTemporaryFilePath(basename: "test") { path in + let strPath = String(decoding: path) + XCTAssert(strPath.contains("test")) + try withTemporaryFilePath(basename: "test") { path2 in + let strPath2 = String(decoding: path2) + XCTAssertNotEqual(strPath, strPath2) + } + } + } + + func testCleanup() throws { + var thePath: FilePath? = nil + + try withTemporaryFilePath(basename: "test") { path in + thePath = path.appending("foo.txt") + let fd = try FileDescriptor.open(thePath!, .readWrite, + options: [.create, .truncate], + permissions: .ownerReadWrite) + _ = try fd.closeAfter { + try fd.writeAll("Hello World".utf8) + } + } + + XCTAssertThrowsError(try FileDescriptor.open(thePath!, .readOnly)) { + error in + + XCTAssertEqual(error as! Errno, Errno.noSuchFileOrDirectory) + } + } +} diff --git a/Tests/SystemTests/FilePathTests/FilePathTest.swift b/Tests/SystemTests/FilePathTests/FilePathTest.swift new file mode 100644 index 00000000..ef71c850 --- /dev/null +++ b/Tests/SystemTests/FilePathTests/FilePathTest.swift @@ -0,0 +1,105 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 +*/ + +import XCTest + +#if SYSTEM_PACKAGE +import SystemPackage +#else +import System +#endif + +@available(System 0.0.1, *) +func filePathFromInvalidCodePointSequence(_ bytes: S) -> FilePath where S.Element == CInterop.PlatformUnicodeEncoding.CodeUnit { + var array = Array(bytes) + assert(array.last != 0, "already null terminated") + array += [0] + + return array.withUnsafeBufferPointer { + $0.withMemoryRebound(to: CInterop.PlatformChar.self) { + FilePath(platformString: $0.baseAddress!) + } + } +} + +@available(System 0.0.2, *) +final class FilePathTest: XCTestCase { + struct TestPath { + let filePath: FilePath + let string: String + let validString: Bool + + init(filePath: FilePath, string: String, validString: Bool) { + self.filePath = filePath + #if os(Windows) + self.string = string.replacingOccurrences(of: "/", with: "\\") + #else + self.string = string + #endif + self.validString = validString + } + } + +#if os(Windows) + static let invalidSequence: [UTF16.CodeUnit] = [0xd800, 0x0020] + static let invalidSequenceTest = + TestPath(filePath: filePathFromInvalidCodePointSequence(invalidSequence), + string: String(decoding: invalidSequence, as: UTF16.self), + validString: false) +#else + static let invalidSequence: [UTF8.CodeUnit] = [0x2F, 0x61, 0x2F, 0x62, 0x2F, 0x83] + static let invalidSequenceTest = + TestPath(filePath: filePathFromInvalidCodePointSequence(invalidSequence), + string: String(decoding: invalidSequence, as: UTF8.self), + validString: false) +#endif + + var testPaths: [TestPath] = [ + // empty + TestPath(filePath: FilePath(), string: String(), validString: true), + + // valid ascii + TestPath(filePath: "/a/b/c", string: "/a/b/c", validString: true), + + // valid utf8 + TestPath(filePath: "/あ/🧟‍♀️", string: "/あ/🧟‍♀️", validString: true), + + // invalid sequence + invalidSequenceTest, + ] + + func testFilePath() { + + XCTAssertEqual(0, FilePath().length) + + for testPath in testPaths { + + XCTAssertEqual(testPath.string, String(decoding: testPath.filePath)) + + // TODO: test component CodeUnit representation validation + if testPath.validString { + XCTAssertEqual(testPath.filePath, FilePath(testPath.string)) + XCTAssertEqual(testPath.string, String(validating: testPath.filePath)) + } else { + XCTAssertNotEqual(testPath.filePath, FilePath(testPath.string)) + XCTAssertNil(String(validating: testPath.filePath)) + } + + testPath.filePath.withPlatformString { +#if os(Windows) + XCTAssertEqual(testPath.string, String(decodingCString: $0, as: UTF16.self)) +#else + XCTAssertEqual(testPath.string, String(cString: $0)) +#endif + XCTAssertEqual(testPath.filePath, FilePath(platformString: $0)) + } + } + } +} + diff --git a/Tests/SystemTests/FileTypesTest.swift b/Tests/SystemTests/FileTypesTest.swift index f6dc7f07..0ddcb0de 100644 --- a/Tests/SystemTests/FileTypesTest.swift +++ b/Tests/SystemTests/FileTypesTest.swift @@ -1,44 +1,71 @@ /* 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 - 2025 Apple Inc. and the Swift System project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information */ import XCTest + +#if SYSTEM_PACKAGE import SystemPackage +#else +import System +#endif +#if canImport(Android) +import Android +#endif -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) final class FileDescriptorTest: XCTestCase { + func testStandardDescriptors() { + XCTAssertEqual(FileDescriptor.standardInput.rawValue, 0) + XCTAssertEqual(FileDescriptor.standardOutput.rawValue, 1) + XCTAssertEqual(FileDescriptor.standardError.rawValue, 2) + } + // Test the constants match the C header values. For various reasons, func testConstants() { XCTAssertEqual(O_RDONLY, FileDescriptor.AccessMode.readOnly.rawValue) XCTAssertEqual(O_WRONLY, FileDescriptor.AccessMode.writeOnly.rawValue) XCTAssertEqual(O_RDWR, FileDescriptor.AccessMode.readWrite.rawValue) +#if !os(WASI) // Would need to use _getConst funcs from CSystem +#if !os(Windows) XCTAssertEqual(O_NONBLOCK, FileDescriptor.OpenOptions.nonBlocking.rawValue) +#endif XCTAssertEqual(O_APPEND, FileDescriptor.OpenOptions.append.rawValue) XCTAssertEqual(O_CREAT, FileDescriptor.OpenOptions.create.rawValue) XCTAssertEqual(O_TRUNC, FileDescriptor.OpenOptions.truncate.rawValue) XCTAssertEqual(O_EXCL, FileDescriptor.OpenOptions.exclusiveCreate.rawValue) +#endif // !os(WASI +#if !os(Windows) XCTAssertEqual(O_NOFOLLOW, FileDescriptor.OpenOptions.noFollow.rawValue) XCTAssertEqual(O_CLOEXEC, FileDescriptor.OpenOptions.closeOnExec.rawValue) +#endif // BSD only -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) XCTAssertEqual(O_SHLOCK, FileDescriptor.OpenOptions.sharedLock.rawValue) XCTAssertEqual(O_EXLOCK, FileDescriptor.OpenOptions.exclusiveLock.rawValue) +#endif + +#if SYSTEM_PACKAGE_DARWIN XCTAssertEqual(O_SYMLINK, FileDescriptor.OpenOptions.symlink.rawValue) XCTAssertEqual(O_EVTONLY, FileDescriptor.OpenOptions.eventOnly.rawValue) #endif +#if os(FreeBSD) + XCTAssertEqual(O_SYNC, FileDescriptor.OpenOptions.sync.rawValue) +#endif + XCTAssertEqual(SEEK_SET, FileDescriptor.SeekOrigin.start.rawValue) XCTAssertEqual(SEEK_CUR, FileDescriptor.SeekOrigin.current.rawValue) XCTAssertEqual(SEEK_END, FileDescriptor.SeekOrigin.end.rawValue) -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) XCTAssertEqual(SEEK_HOLE, FileDescriptor.SeekOrigin.nextHole.rawValue) XCTAssertEqual(SEEK_DATA, FileDescriptor.SeekOrigin.nextData.rawValue) #endif @@ -49,7 +76,7 @@ final class FileDescriptorTest: XCTestCase { } -// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +@available(System 0.0.1, *) final class FilePermissionsTest: XCTestCase { func testPermissions() { diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift new file mode 100644 index 00000000..9246d394 --- /dev/null +++ b/Tests/SystemTests/IORequestTests.swift @@ -0,0 +1,34 @@ +#if compiler(>=6.2) && $Lifetimes +#if os(Linux) + +import XCTest + +#if SYSTEM_PACKAGE + @testable import SystemPackage +#else + import System +#endif + +func requestBytes(_ request: consuming RawIORequest) -> [UInt8] { + return withUnsafePointer(to: request.rawValue) { + let requestBuf = UnsafeBufferPointer(start: $0, count: 1) + let rawBytes = UnsafeRawBufferPointer(requestBuf) + return .init(rawBytes) + } +} + +// This test suite compares various IORequests bit-for-bit to IORequests +// that were generated with liburing or manually written out, +// which are known to work correctly. +final class IORequestTests: XCTestCase { + func testNop() { + let req = IORing.Request.nop().makeRawRequest() + let sourceBytes = requestBytes(req) + // convenient property of nop: it's all zeros! + // for some unknown reason, liburing sets the fd field to -1. + // we're not trying to be bug-compatible with it, so 0 *should* work. + XCTAssertEqual(sourceBytes, .init(repeating: 0, count: 64)) + } +} +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift new file mode 100644 index 00000000..c5d54d41 --- /dev/null +++ b/Tests/SystemTests/IORingTests.swift @@ -0,0 +1,132 @@ +#if compiler(>=6.2) && $Lifetimes +#if os(Linux) + +import XCTest +import CSystem //for eventfd + +#if SYSTEM_PACKAGE +import SystemPackage +#else +import System +#endif + +func uringEnabled() throws -> Bool { + do { + let procPath = FilePath("/proc/sys/kernel/io_uring_disabled") + let fd = try FileDescriptor.open(procPath, .readOnly) + let buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1024, alignment: 0) + _ = try fd.read(into: buffer) + if buffer.load(fromByteOffset: 0, as: Int.self) == 0 { + return true + } + } catch (_) { + return false + } + return false +} + +final class IORingTests: XCTestCase { + func testInit() throws { + guard try uringEnabled() else { return } + _ = try IORing(queueDepth: 32, flags: []) + } + + func testNop() throws { + guard try uringEnabled() else { return } + var ring = try IORing(queueDepth: 32, flags: []) + _ = try ring.submit(linkedRequests: .nop()) + let completion = try ring.blockingConsumeCompletion() + XCTAssertEqual(completion.result, 0) + } + + func makeHelloWorldFile() throws -> (dir: FileDescriptor, file: FilePath) { + mkdir("/tmp/IORingTests/", 0o777) + let path: FilePath = "/tmp/IORingTests/test.txt" + let fd = try FileDescriptor.open( + path, + .readWrite, + options: .create, + permissions: .ownerReadWrite + ) + try fd.writeAll("Hello, World!".utf8) + try fd.close() + let parent = try FileDescriptor.open("/tmp/IORingTests/", .readOnly) + + return (parent, path) + } + + func cleanUpHelloWorldFile(_ parent: FileDescriptor) throws { + try parent.close() + rmdir("/tmp/IORingTests/") + } + + func setupTestRing(depth: Int, fileSlots: Int, buffers: [UnsafeMutableRawBufferPointer]) throws -> IORing { + var ring: IORing = try IORing(queueDepth: UInt32(depth)) + _ = try ring.registerFileSlots(count: 1) + _ = try ring.registerBuffers(buffers) + return ring + } + + func testUndersizedSubmissionQueue() throws { + guard try uringEnabled() else { return } + var ring: IORing = try IORing(queueDepth: 1) + let enqueued = ring.prepare(linkedRequests: .nop(), .nop()) + XCTAssertFalse(enqueued) + } + + // Exercises opening, reading, closing, registered files, registered buffers, and eventfd + func testOpenReadAndWriteFixedFile() throws { + guard try uringEnabled() else { return } + let (parent, path) = try makeHelloWorldFile() + let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 13, alignment: 16) + var ring = try setupTestRing(depth: 6, fileSlots: 1, buffers: [rawBuffer]) + let eventFD = FileDescriptor(rawValue: eventfd(0, Int32(EFD_SEMAPHORE))) + try ring.registerEventFD(eventFD) + + //Part 1: read the file we just created, and make sure the eventfd fires + let enqueued = try ring.submit(linkedRequests: + .open(path, in: parent, into: ring.registeredFileSlots[0], mode: .readOnly), + .read(ring.registeredFileSlots[0], into: ring.registeredBuffers[0]), + .close(ring.registeredFileSlots[0])) + XCTAssert(enqueued) + let efdBuf = UnsafeMutableRawBufferPointer.allocate(byteCount: 8, alignment: 0) + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //open + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //read + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //close + let result = String(cString: rawBuffer.assumingMemoryBound(to: CChar.self).baseAddress!) + XCTAssertEqual(result, "Hello, World!") + + //Part 2: delete that file, then use the ring to write out a new one + let rmResult = path.withPlatformString { + remove($0) + } + XCTAssertEqual(rmResult, 0) + let enqueued2 = try ring.submit(linkedRequests: + .open(path, in: parent, into: ring.registeredFileSlots[0], mode: .readWrite, options: .create, permissions: .ownerReadWrite), + .write(ring.registeredBuffers[0], into: ring.registeredFileSlots[0]), + .close(ring.registeredFileSlots[0])) + XCTAssert(enqueued2) + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //open + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //write + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //close + memset(rawBuffer.baseAddress!, 0, rawBuffer.count) + //Verify using a non-ring IO method that what we wrote matches our expectations + print("about to open") + let nonRingFD = try FileDescriptor.open(path, .readOnly) + let bytesRead = try nonRingFD.read(into: rawBuffer) + XCTAssert(bytesRead == 13) + let result2 = String(cString: rawBuffer.assumingMemoryBound(to: CChar.self).baseAddress!) + XCTAssertEqual(result2, "Hello, World!") + try cleanUpHelloWorldFile(parent) + efdBuf.deallocate() + rawBuffer.deallocate() + } +} +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Tests/SystemTests/MachPortTests.swift b/Tests/SystemTests/MachPortTests.swift new file mode 100644 index 00000000..7ecf8641 --- /dev/null +++ b/Tests/SystemTests/MachPortTests.swift @@ -0,0 +1,242 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2022 - 2025 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +#if SYSTEM_PACKAGE_DARWIN + +import XCTest +import Darwin.Mach + +#if SYSTEM_PACKAGE +import SystemPackage +#else +import System +#endif + +@available(System 1.4.0, *) +final class MachPortTests: XCTestCase { + func refCountForMachPortName(name:mach_port_name_t, kind:mach_port_right_t) -> mach_port_urefs_t { + var refCount:mach_port_urefs_t = .max + let kr = mach_port_get_refs(mach_task_self_, name, kind, &refCount) + if kr == KERN_INVALID_NAME { + refCount = 0 + } else { + XCTAssertEqual(kr, KERN_SUCCESS) + } + return refCount + } + + func scopedReceiveRight(name:mach_port_name_t) -> mach_port_urefs_t { + let right = Mach.Port(name:name) // this should automatically deallocate when going out of scope + defer { _ = right } + return refCountForMachPortName(name:name, kind:MACH_PORT_RIGHT_RECEIVE) + } + + func testReceiveRightDeallocation() throws { + var name: mach_port_name_t = 0xFFFFFFFF + let kr = mach_port_allocate(mach_task_self_, MACH_PORT_RIGHT_RECEIVE, &name) + XCTAssertEqual(kr, KERN_SUCCESS) + + XCTAssertNotEqual(name, 0xFFFFFFFF) + + let originalCount = refCountForMachPortName(name: name, kind: MACH_PORT_RIGHT_RECEIVE) + XCTAssertEqual(originalCount, 1) + + let incrementedCount = scopedReceiveRight(name:name) + XCTAssertEqual(incrementedCount, 1); + + let deallocated = refCountForMachPortName(name:name, kind: MACH_PORT_RIGHT_RECEIVE) + XCTAssertEqual(deallocated, 0); + } + + func consumeSendRightAutomatically(name:mach_port_name_t) -> mach_port_urefs_t { + let send = Mach.Port(name:name) // this should automatically deallocate when going out of scope + return send.withBorrowedName { name in + // Get the ref count before automatic deallocation happens + return refCountForMachPortName(name:name, kind:MACH_PORT_RIGHT_SEND) + } + } + + func testSendRightDeallocation() throws { + let recv = Mach.Port() + recv.withBorrowedName { name in + let kr = mach_port_insert_right(mach_task_self_, name, name, mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND)) + XCTAssertEqual(kr, KERN_SUCCESS) + let one = consumeSendRightAutomatically(name:name) + XCTAssertEqual(one, 1); + let zero = refCountForMachPortName(name:name, kind:MACH_PORT_RIGHT_SEND) + XCTAssertEqual(zero, 0); + } + } + + func testSendRightRelinquishment() throws { + let recv = Mach.Port() + + let name = ({ + let send = recv.makeSendRight() + let one = send.withBorrowedName { name in + return self.refCountForMachPortName(name:name, kind:MACH_PORT_RIGHT_SEND) + } + XCTAssertEqual(one, 1) + + return send.relinquish() + })() + + let stillOne = refCountForMachPortName(name:name, kind:MACH_PORT_RIGHT_SEND) + XCTAssertEqual(stillOne, 1) + + recv.withBorrowedName { + let alsoOne = refCountForMachPortName(name: $0, kind: MACH_PORT_RIGHT_RECEIVE) + XCTAssertEqual(alsoOne, 1) + } + } + + func testSendOnceRightRelinquishment() throws { + let recv = Mach.Port() + + let name = ({ + let send = recv.makeSendOnceRight() + let one = send.withBorrowedName { name in + return self.refCountForMachPortName(name: name, kind: MACH_PORT_RIGHT_SEND_ONCE) + } + XCTAssertEqual(one, 1) + + return send.relinquish() + })() + + let stillOne = refCountForMachPortName(name: name, kind: MACH_PORT_RIGHT_SEND_ONCE) + XCTAssertEqual(stillOne, 1) + + recv.withBorrowedName { + let alsoOne = refCountForMachPortName(name: $0, kind: MACH_PORT_RIGHT_RECEIVE) + XCTAssertEqual(alsoOne, 1) + } + } + + func testReceiveRightRelinquishment() throws { + let recv = Mach.Port() + + let one = recv.withBorrowedName { + self.refCountForMachPortName(name: $0, kind: MACH_PORT_RIGHT_RECEIVE) + } + XCTAssertEqual(one, 1) + + let name = recv.unguardAndRelinquish() + + let stillOne = refCountForMachPortName(name: name, kind: MACH_PORT_RIGHT_RECEIVE) + XCTAssertEqual(stillOne, 1) + } + + func testMakeSendCountSettable() throws { + var recv = Mach.Port() + XCTAssertEqual(recv.makeSendCount, 0) + recv.makeSendCount = 7 + XCTAssertEqual(recv.makeSendCount, 7) + } + + func makeSendRight() throws -> Mach.Port { + let recv = Mach.Port() + let zero = recv.makeSendCount + XCTAssertEqual(zero, 0) + let send = recv.makeSendRight() + let one = recv.makeSendCount + XCTAssertEqual(one, 1) + return send + } + + func testMakeSendCountIncrement() throws { + _ = try makeSendRight() + } + + func testMakeSendOnceDoesntIncrementMakeSendCount() throws { + let recv = Mach.Port() + let zero = recv.makeSendCount + XCTAssertEqual(zero, 0) + _ = recv.makeSendOnceRight() + let same = recv.makeSendCount + XCTAssertEqual(same, zero) + } + + func testMakeSendOnceIsUnique() throws { + let recv = Mach.Port() + let once = recv.makeSendOnceRight() + recv.withBorrowedName { rname in + once.withBorrowedName { oname in + XCTAssertNotEqual(oname, rname) + } + } + } + + func testCopySend() throws { + let recv = Mach.Port() + let zero = recv.makeSendCount + XCTAssertEqual(zero, 0) + let send = recv.makeSendRight() + let one = recv.makeSendCount + XCTAssertEqual(one, 1) + _ = try send.copySendRight() + let same = recv.makeSendCount + XCTAssertEqual(same, one) + + } + + func testCopyDeadName() throws { + let recv = Mach.Port() + let send = recv.makeSendRight() + _ = consume recv // and turn `send` into a dead name + XCTAssertThrowsError( + _ = try send.copySendRight(), + "Copying a dead name should throw" + ) { error in + XCTAssertEqual( + error as! Mach.PortRightError, Mach.PortRightError.deadName + ) + } + } + + func testCopyDeadName2() throws { + let send = Mach.Port(name: 0xffffffff) + XCTAssertThrowsError( + _ = try send.copySendRight(), + "Copying a dead name should throw" + ) { error in + XCTAssertEqual( + error as! Mach.PortRightError, Mach.PortRightError.deadName + ) + } + } + + func testMakeReceiveRightFromExistingName() throws { + var name = mach_port_name_t(MACH_PORT_NULL) + var kr = mach_port_allocate(mach_task_self_, MACH_PORT_RIGHT_RECEIVE, &name) + XCTAssertEqual(kr, KERN_SUCCESS) + XCTAssertNotEqual(name, mach_port_name_t(MACH_PORT_NULL)) + let context = mach_port_context_t(arc4random()) + kr = mach_port_guard(mach_task_self_, name, context, 0) + XCTAssertEqual(kr, KERN_SUCCESS) + + let right = Mach.Port(name: name, context: context) + right.withBorrowedName { + XCTAssertEqual(name, $0) + XCTAssertEqual(context, $1) + } + } + + func testDeinitDeadSendRights() throws { + let recv = Mach.Port() + let send = recv.makeSendRight() + let send1 = recv.makeSendOnceRight() + + _ = consume recv + // `send` and `send1` have become dead names + _ = consume send + _ = consume send1 + } +} + +#endif diff --git a/Tests/SystemTests/MockingTest.swift b/Tests/SystemTests/MockingTest.swift index 87b55c04..9576ed8f 100644 --- a/Tests/SystemTests/MockingTest.swift +++ b/Tests/SystemTests/MockingTest.swift @@ -7,11 +7,16 @@ See https://swift.org/LICENSE.txt for license information */ +#if ENABLE_MOCKING import XCTest -import SystemPackage -@testable import SystemInternals -// @available... +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@available(System 0.0.2, *) final class MockingTest: XCTestCase { func testMocking() { XCTAssertFalse(mockingEnabled) @@ -44,3 +49,4 @@ final class MockingTest: XCTestCase { XCTAssertFalse(mockingEnabled) } } +#endif // ENABLE_MOCKING diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift new file mode 100644 index 00000000..524226d2 --- /dev/null +++ b/Tests/SystemTests/StatTests.swift @@ -0,0 +1,420 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift System project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if !os(Windows) + +import Testing + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import CSystem +import Glibc +#elseif canImport(Musl) +import CSystem +import Musl +#elseif canImport(WASILibc) +import CSystem +import WASILibc +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@Suite("Stat") +private struct StatTests { + + @available(System 99, *) + @Test func basics() async throws { + try withTemporaryFilePath(basename: "Stat_basics") { tempDir in + let dirStatFromFilePath = try tempDir.stat() + #expect(dirStatFromFilePath.type == .directory) + + let dirFD = try FileDescriptor.open(tempDir, .readOnly) + defer { + try? dirFD.close() + } + let dirStatFromFD = try dirFD.stat() + #expect(dirStatFromFD.type == .directory) + + let dirStatFromCString = try tempDir.withPlatformString { try Stat($0) } + #expect(dirStatFromCString.type == .directory) + + #expect(dirStatFromFilePath == dirStatFromFD) + #expect(dirStatFromFD == dirStatFromCString) + + let tempFile = tempDir.appending("test.txt") + let fileFD = try FileDescriptor.open(tempFile, .readWrite, options: .create, permissions: [.ownerReadWrite, .groupRead, .otherRead]) + defer { + try? fileFD.close() + } + try fileFD.writeAll("Hello, world!".utf8) + + let fileStatFromFD = try fileFD.stat() + #expect(fileStatFromFD.type == .regular) + #expect(fileStatFromFD.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + #expect(fileStatFromFD.size == "Hello, world!".utf8.count) + + let fileStatFromFilePath = try tempFile.stat() + #expect(fileStatFromFilePath.type == .regular) + + let fileStatFromCString = try tempFile.withPlatformString { try Stat($0) } + #expect(fileStatFromCString.type == .regular) + + #expect(fileStatFromFD == fileStatFromFilePath) + #expect(fileStatFromFilePath == fileStatFromCString) + } + } + + @available(System 99, *) + @Test func followSymlinkInits() async throws { + try withTemporaryFilePath(basename: "Stat_followSymlinkInits") { tempDir in + let targetFilePath = tempDir.appending("target.txt") + let symlinkPath = tempDir.appending("symlink") + let targetFD = try FileDescriptor.open(targetFilePath, .readWrite, options: .create, permissions: .ownerReadWrite) + defer { + try? targetFD.close() + } + try targetFD.writeAll(Array(repeating: UInt8(ascii: "A"), count: 1025)) + + try targetFilePath.withPlatformString { targetPtr in + try symlinkPath.withPlatformString { symlinkPtr in + try #require(symlink(targetPtr, symlinkPtr) == 0, "\(Errno.current)") + } + } + + // Can't open an fd to a symlink on WASI (no O_PATH) + // On non-Darwin, we need O_PATH | O_NOFOLLOW to open the symlink + // directly, but O_PATH requires _GNU_SOURCE be defined (TODO). + #if SYSTEM_PACKAGE_DARWIN + let symlinkFD = try FileDescriptor.open(symlinkPath, .readOnly, options: .symlink) + defer { + try? symlinkFD.close() + } + #endif + + let targetStat = try targetFilePath.stat() + let originalTargetAccessTime = targetStat.st_atim + + let symlinkStat = try symlinkPath.stat(followTargetSymlink: false) + let originalSymlinkAccessTime = symlinkStat.st_atim + + #expect(targetStat != symlinkStat) + #expect(targetStat.type == .regular) + #expect(symlinkStat.type == .symbolicLink) + #expect(symlinkStat.size < targetStat.size) + #expect(symlinkStat.sizeAllocated < targetStat.sizeAllocated) + + // Set each .st_atim back to its original value for comparison + + // FileDescriptor Extensions + + var stat = try targetFD.stat() + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + #if SYSTEM_PACKAGE_DARWIN + stat = try symlinkFD.stat() + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + #endif + + // Initializing Stat with FileDescriptor + + stat = try Stat(targetFD) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + #if SYSTEM_PACKAGE_DARWIN + stat = try Stat(symlinkFD) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + #endif + + // FilePath Extensions + + stat = try symlinkPath.stat(followTargetSymlink: true) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try symlinkPath.stat(followTargetSymlink: false) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + + // Initializing Stat with UnsafePointer + + try symlinkPath.withPlatformString { pathPtr in + stat = try Stat(pathPtr, followTargetSymlink: true) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(pathPtr, followTargetSymlink: false) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + } + + // Initializing Stat with FilePath + + stat = try Stat(symlinkPath, followTargetSymlink: true) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(symlinkPath, followTargetSymlink: false) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + + // Initializing Stat with String + + stat = try Stat(symlinkPath.string, followTargetSymlink: true) + stat.st_atim = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(symlinkPath.string, followTargetSymlink: false) + stat.st_atim = originalSymlinkAccessTime + #expect(stat == symlinkStat) + } + } + + @available(System 99, *) + @Test func permissions() async throws { + try withTemporaryFilePath(basename: "Stat_permissions") { tempDir in + let testFile = tempDir.appending("test.txt") + let fd = try FileDescriptor.open(testFile, .writeOnly, options: .create, permissions: [.ownerReadWrite, .groupRead, .otherRead]) + try fd.close() + + let stat = try testFile.stat() + #expect(stat.type == .regular) + #expect(stat.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + + var newMode = stat.mode + newMode.permissions.insert(.ownerExecute) + try testFile.withPlatformString { pathPtr in + try #require(chmod(pathPtr, newMode.permissions.rawValue) == 0, "\(Errno.current)") + } + + let updatedStat = try testFile.stat() + #expect(updatedStat.permissions == newMode.permissions) + + newMode.permissions.remove(.ownerWriteExecute) + try testFile.withPlatformString { pathPtr in + try #require(chmod(pathPtr, newMode.permissions.rawValue) == 0, "\(Errno.current)") + } + + let readOnlyStat = try testFile.stat() + #expect(readOnlyStat.permissions == newMode.permissions) + } + } + + @available(System 99, *) + @Test func times() async throws { + var start = timespec() + try #require(clock_gettime(CLOCK_REALTIME, &start) == 0, "\(Errno.current)") + start.tv_sec -= 1 // A little wiggle room + try withTemporaryFilePath(basename: "Stat_times") { tempDir in + var dirStat = try tempDir.stat() + let dirAccessTime0 = dirStat.st_atim + let dirModificationTime0 = dirStat.st_mtim + let dirChangeTime0 = dirStat.st_ctim + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let dirCreationTime0 = dirStat.st_birthtim + #endif + + var startUpperBound = start + startUpperBound.tv_sec += 5 + #expect(dirAccessTime0 >= start) + #expect(dirAccessTime0 < startUpperBound) + #expect(dirModificationTime0 >= start) + #expect(dirModificationTime0 < startUpperBound) + #expect(dirChangeTime0 >= start) + #expect(dirChangeTime0 < startUpperBound) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirCreationTime0 >= start) + #expect(dirCreationTime0 < startUpperBound) + #endif + + // Fails intermittently if less than 5ms + usleep(10000) + + let file1 = tempDir.appending("test1.txt") + let fd1 = try FileDescriptor.open(file1, .writeOnly, options: .create, permissions: .ownerReadWrite) + defer { + try? fd1.close() + } + + dirStat = try tempDir.stat() + let dirAccessTime1 = dirStat.st_atim + let dirModificationTime1 = dirStat.st_mtim + let dirChangeTime1 = dirStat.st_ctim + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let dirCreationTime1 = dirStat.st_birthtim + #endif + + // Creating a file updates directory modification and change time. + // Access time may not be updated depending on mount options like NOATIME. + + #expect(dirModificationTime1 > dirModificationTime0) + #expect(dirChangeTime1 > dirChangeTime0) + #expect(dirAccessTime1 >= dirAccessTime0) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirCreationTime1 == dirCreationTime0) + #endif + + usleep(10000) + + // Changing permissions only updates directory change time + + try tempDir.withPlatformString { pathPtr in + var newMode = dirStat.mode + // tempDir only starts with .ownerReadWriteExecute + newMode.permissions.insert(.groupReadWriteExecute) + try #require(chmod(pathPtr, newMode.rawValue) == 0, "\(Errno.current)") + } + + dirStat = try tempDir.stat() + let dirChangeTime2 = dirStat.st_ctim + #expect(dirChangeTime2 > dirChangeTime1) + #expect(dirStat.st_atim == dirAccessTime1) + #expect(dirStat.st_mtim == dirModificationTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirStat.st_birthtim == dirCreationTime1) + #endif + + var stat1 = try file1.stat() + let file1AccessTime1 = stat1.st_atim + let file1ModificationTime1 = stat1.st_mtim + let file1ChangeTime1 = stat1.st_ctim + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let file1CreationTime1 = stat1.st_birthtim + #endif + + usleep(10000) + + try fd1.writeAll("Hello, world!".utf8) + stat1 = try file1.stat() + let file1AccessTime2 = stat1.st_atim + let file1ModificationTime2 = stat1.st_mtim + let file1ChangeTime2 = stat1.st_ctim + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let file1CreationTime2 = stat1.st_birthtim + #endif + + #expect(file1AccessTime2 >= file1AccessTime1) + #expect(file1ModificationTime2 > file1ModificationTime1) + #expect(file1ChangeTime2 > file1ChangeTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(file1CreationTime2 == file1CreationTime1) + #endif + + // Changing file metadata or content does not update directory times + + dirStat = try tempDir.stat() + #expect(dirStat.st_ctim == dirChangeTime2) + #expect(dirStat.st_atim == dirAccessTime1) + #expect(dirStat.st_mtim == dirModificationTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirStat.st_birthtim == dirCreationTime1) + #endif + + usleep(10000) + + let file2 = tempDir.appending("test2.txt") + let fd2 = try FileDescriptor.open(file2, .writeOnly, options: .create, permissions: .ownerReadWrite) + defer { + try? fd2.close() + } + + let stat2 = try file2.stat() + #expect(stat2.st_atim > file1AccessTime2) + #expect(stat2.st_mtim > file1ModificationTime2) + #expect(stat2.st_ctim > file1ChangeTime2) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(stat2.st_birthtim > file1CreationTime2) + #endif + } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + @available(System 99, *) + @Test func flags() async throws { + try withTemporaryFilePath(basename: "Stat_flags") { tempDir in + let filePath = tempDir.appending("test.txt") + let fd = try FileDescriptor.open(filePath, .writeOnly, options: .create, permissions: .ownerReadWrite) + defer { + try? fd.close() + } + var stat = try fd.stat() + var flags = stat.flags + + #if SYSTEM_PACKAGE_DARWIN + let userSettableFlags: FileFlags = [ + .noDump, .userImmutable, .userAppend, + .opaque, .tracked, .hidden, + /* .dataVault (throws EPERM when testing) */ + ] + #elseif os(FreeBSD) + let userSettableFlags: FileFlags = [ + .noDump, .userImmutable, .userAppend, + .opaque, .tracked, .hidden, + .userNoUnlink, + .offline, + .readOnly, + .reparse, + .sparse, + .system + ] + #else // os(OpenBSD) + let userSettableFlags: FileFlags = [ + .noDump, .userImmutable, .userAppend + ] + #endif + + flags.insert(userSettableFlags) + try #require(fchflags(fd.rawValue, flags.rawValue) == 0, "\(Errno.current)") + + stat = try fd.stat() + #expect(stat.flags == flags) + + flags.remove(userSettableFlags) + try #require(fchflags(fd.rawValue, flags.rawValue) == 0, "\(Errno.current)") + + stat = try fd.stat() + #expect(stat.flags == flags) + } + } + #endif + +} + +// Comparison operators for timespec until UTCClock.Instant properties are available +private func >= (lhs: timespec, rhs: timespec) -> Bool { + (lhs.tv_sec, lhs.tv_nsec) >= (rhs.tv_sec, rhs.tv_nsec) +} + +private func < (lhs: timespec, rhs: timespec) -> Bool { + (lhs.tv_sec, lhs.tv_nsec) < (rhs.tv_sec, rhs.tv_nsec) +} + +private func > (lhs: timespec, rhs: timespec) -> Bool { + (lhs.tv_sec, lhs.tv_nsec) > (rhs.tv_sec, rhs.tv_nsec) +} + +private func == (lhs: timespec, rhs: timespec) -> Bool { + lhs.tv_sec == rhs.tv_sec && lhs.tv_nsec == rhs.tv_nsec +} + +#endif diff --git a/Tests/SystemTests/SystemCharTest.swift b/Tests/SystemTests/SystemCharTest.swift new file mode 100644 index 00000000..0ec5411e --- /dev/null +++ b/Tests/SystemTests/SystemCharTest.swift @@ -0,0 +1,39 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2021 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 +*/ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +final class SystemCharTest: XCTestCase { + func testIsLetter() { + let valid = SystemString( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + for char in valid { + XCTAssertTrue(char.isLetter) + } + + // non printable + for value in 0..<(UInt8(ascii: " ")) { + XCTAssertFalse(SystemChar(codeUnit: CInterop.PlatformUnicodeEncoding.CodeUnit(value)).isLetter) + } + XCTAssertFalse(SystemChar(codeUnit: 0x7F).isLetter) // DEL + + // misc other + let invalid = SystemString( + ##" !"#$%&'()*+,-./0123456789:;<=>?@[\]^_`{|}~"##) + for char in invalid { + XCTAssertFalse(char.isLetter) + } + } +} diff --git a/Tests/SystemTests/SystemStringTests.swift b/Tests/SystemTests/SystemStringTests.swift new file mode 100644 index 00000000..48ca83c1 --- /dev/null +++ b/Tests/SystemTests/SystemStringTests.swift @@ -0,0 +1,425 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2024 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 +*/ + +// Tests for PlatformString, SystemString, and FilePath's forwarding APIs + +// TODO: Adapt test to Windows + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +private func makeRaw( + _ str: String +) -> [CInterop.PlatformChar] { + var str = str +#if os(Windows) + var array = Array(str.utf16) +#else + var array = str.withUTF8 { $0.withMemoryRebound(to: CChar.self, Array.init) } +#endif + array.append(0) + return array +} +private func makeStr( + _ raw: [CInterop.PlatformChar] +) -> String { + precondition((raw.last ?? 0) == 0) + // FIXME: Unfortunately, this relies on some of the same mechanisms tested... + return raw.withUnsafeBufferPointer { + String(platformString: $0.baseAddress!) + } +} + +struct StringTest: TestCase { + // The error-corrected string + var string: String + + // The raw contents + var raw: [CInterop.PlatformChar] + + var isValid: Bool + + var file: StaticString + var line: UInt + + func runAllTests() { + precondition((raw.last ?? 0) == 0, "invalid test case") + + // Test idempotence post-validation + string.withPlatformString { + if isValid { + let len = system_platform_strlen($0) + expectEqual(raw.count, 1+len, "Validation idempotence") + expectEqualSequence(raw, UnsafeBufferPointer(start: $0, count: 1+len), + "Validation idempotence") + } + + let str = String(platformString: $0) + expectEqualSequence( + string.unicodeScalars, str.unicodeScalars, "Validation idempotence") + + let validStr = String(validatingPlatformString: $0) + expectEqual(string, validStr, "Validation idempotence") + } + + // Test String, SystemString, FilePath construction + let sysStr = SystemString(string) + expectEqualSequence(string.unicodeScalars, sysStr.string.unicodeScalars) + expectEqual(string, String(decoding: sysStr)) + expectEqual(string, String(validating: sysStr)) + + let sysRaw = raw.withUnsafeBufferPointer { + SystemString(platformString: $0.baseAddress!) + } + expectEqual(string, String(decoding: sysRaw)) + expectEqual(isValid, nil != String(validating: sysRaw)) + expectEqual(isValid, sysStr == sysRaw) + + let strDecoded = raw.withUnsafeBufferPointer { + String(platformString: $0.baseAddress!) + } + expectEqualSequence(string.unicodeScalars, strDecoded.unicodeScalars) + + let strValidated = raw.withUnsafeBufferPointer { + String(validatingPlatformString: $0.baseAddress!) + } + expectEqual(isValid, strValidated != nil, "String(validatingPlatformString:)") + expectEqual(isValid, strDecoded == strValidated, + "String(validatingPlatformString:)") + + // Test null insertion + let rawChars = raw.lazy.map { SystemChar($0) } + expectEqual(sysRaw, SystemString(rawChars.dropLast())) + sysRaw.withNullTerminatedSystemChars { + expectEqualSequence(rawChars, $0, "rawChars") + } + + // Whether the content has normalized separators + let hasNormalSeparators: Bool = { + var normalSys = sysRaw + normalSys._normalizeSeparators() + return normalSys == sysRaw + }() + + let fpStr = FilePath(string) + if hasNormalSeparators { + expectEqualSequence( + string.unicodeScalars, fpStr.string.unicodeScalars, "FilePath from string") + expectEqual(string, String(decoding: fpStr), "FilePath from string") + expectEqual(string, String(validating: fpStr), "FilePath from string") + expectEqual(sysStr, fpStr._storage, "FilePath from string") + } + + let fpRaw = FilePath(sysRaw) + if hasNormalSeparators { + expectEqual(string, String(decoding: fpRaw), "raw FilePath") + expectEqual(isValid, nil != String(validating: fpRaw), "raw FilePath") + expectEqual(sysRaw, fpRaw._storage, "raw FilePath") + expectEqual(isValid, fpStr == fpRaw, "raw FilePath") + } + fpRaw.withPlatformString { fp0 in + fpRaw._storage.withPlatformString { storage0 in + expectEqual(fp0, storage0, + "FilePath withPlatformString address forwarding") + } + } + + let isComponent = string == fpRaw.components.first?.string + + if hasNormalSeparators && isComponent { + // Test FilePath.Component + let compStr = FilePath.Component(string)! + expectEqualSequence( + string.unicodeScalars, compStr.string.unicodeScalars, "Component from string") + expectEqual(string, String(decoding: compStr), "Component from string") + expectEqual(string, String(validating: compStr), "Component from string") + expectEqual(sysStr, compStr._slice.base, "Component from string") + + let compRaw = FilePath.Component(sysRaw)! + expectEqual(string, String(decoding: compRaw), "raw Component") + expectEqual(isValid, nil != String(validating: compRaw), "raw Component") + expectEqual(sysRaw, compRaw._slice.base, "raw Component") + expectEqual(isValid, compStr == compRaw, "raw Component") + + // TODO: Below works after we add last component optimization + // compRaw.withPlatformString { fp0 in + // compRaw.slice.base.withPlatformString { storage0 in + // expectEqual(fp0, storage0, + // "Component withPlatformString address forwarding") + // } + // } + + } + + sysRaw.withPlatformString { + let len = system_platform_strlen($0) + expectEqual(raw.count, 1+len, "SystemString.withPlatformString") + expectEqualSequence(raw, UnsafeBufferPointer(start: $0, count: 1+len), + "SystemString.withPlatformString") + if hasNormalSeparators { + expectEqual(sysRaw, FilePath(platformString: $0)._storage) + if isComponent { + expectEqual( + sysRaw, FilePath.Component(platformString: $0)!._slice.base) + } + } + } + + } +} + +extension StringTest { + + static func valid( + _ str: String, + file: StaticString = #file, line: UInt = #line + ) -> StringTest { + StringTest( + string: str, + raw: makeRaw(str), + isValid: true, + file: file, line: line) + } + + static func invalid( + _ raw: [CInterop.PlatformChar], + file: StaticString = #file, line: UInt = #line + ) -> StringTest { + StringTest( + string: makeStr(raw), + raw: raw, + isValid: false, + file: file, line: line) + } +} + +final class SystemStringTest: XCTestCase { + + let validCases: Array = [ + .valid(""), + .valid("a"), + .valid("abc"), + .valid("/あ/🧟‍♀️"), + .valid("/あ\\🧟‍♀️"), + .valid("/あ\\🧟‍♀️///"), + .valid("あ_🧟‍♀️"), + .valid("/a/b/c"), + .valid("/a/b/c//"), + .valid(#"\a\b\c\\"#), + .valid("_a_b_c"), + ] + + #if os(Windows) + let invalidCases: Array = [ + // TODO: Unpaired surrogates + ] + #else + let invalidCases: Array = [ + .invalid([CChar(bitPattern: 0x80), 0]), + .invalid([CChar(bitPattern: 0xFF), 0]), + .invalid([0x2F, 0x61, 0x2F, 0x62, 0x2F, CChar(bitPattern: 0x83), 0]), + ] + #endif + + func testPlatformString() { + for test in validCases { + test.runAllTests() + } + for test in invalidCases { + test.runAllTests() + } + } + + // TODO: More exhaustive RAC+RRC SystemString tests + + func testAdHoc() { + var str: SystemString = "abc" + str.append(SystemChar(ascii: "d")) + XCTAssert(str == "abcd") + XCTAssert(str.count == 4) + XCTAssert(str.count == str.length) + + str.reserveCapacity(100) + XCTAssert(str == "abcd") + } + + func testStringProperty() { + let source: [CInterop.PlatformChar] = [0x61, 0x62, 0, 0x63] + let str = SystemString(platformString: source) + XCTAssertEqual(str.string, str[...].string) + } +} + +extension SystemStringTest { + func test_String_initWithArrayConversion() { + let source: [CInterop.PlatformChar] = [0x61, 0x62, 0, 0x63] + let str = String(platformString: source) + source.withUnsafeBufferPointer { + XCTAssertEqual(str, String(platformString: $0.baseAddress!)) + } + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_String_initWithStringConversion() { + let source = "ab\0c" + var str: String + str = String(platformString: source) + source.withPlatformString { + XCTAssertEqual(str, String(platformString: $0)) + } + str = String(platformString: "") + XCTAssertEqual(str.isEmpty, true) + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_String_initWithInoutConversion() { + var c: CInterop.PlatformChar = 0 + let str = String(platformString: &c) + // Any other value of `c` would violate the null-terminated precondition + XCTAssertEqual(str.isEmpty, true) + } + + func test_String_validatingPlatformStringWithArrayConversion() { + var source: [CInterop.PlatformChar] = [0x61, 0x62, 0, 0x63] + var str: String? + str = String(validatingPlatformString: source) + source.withUnsafeBufferPointer { + XCTAssertEqual(str, String(validatingPlatformString: $0.baseAddress!)) + } + source[1] = CInterop.PlatformChar(truncatingIfNeeded: 0xdfff) + str = String(validatingPlatformString: source) + XCTAssertNil(str) + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_String_validatingPlatformStringWithStringConversion() { + let source = "ab\0c" + var str: String? + str = String(validatingPlatformString: source) + XCTAssertNotNil(str) + source.withPlatformString { + XCTAssertEqual(str, String.init(validatingPlatformString: $0)) + } + str = String(validatingPlatformString: "") + XCTAssertNotNil(str) + XCTAssertEqual(str?.isEmpty, true) + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_String_validatingPlatformStringWithInoutConversion() { + var c: CInterop.PlatformChar = 0 + let str = String(validatingPlatformString: &c) + // Any other value of `c` would violate the null-terminated precondition + XCTAssertNotNil(str) + XCTAssertEqual(str?.isEmpty, true) + } + + func test_FilePath_initWithArrayConversion() { + let source: [CInterop.PlatformChar] = [0x61, 0x62, 0, 0x63] + let path = FilePath(platformString: source) + source.withUnsafeBufferPointer { + XCTAssertEqual(path, FilePath(platformString: $0.baseAddress!)) + } + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_FilePath_initWithStringConversion() { + let source = "ab\0c" + var path: FilePath + path = FilePath(platformString: source) + source.withPlatformString { + XCTAssertEqual(path, FilePath(platformString: $0)) + } + path = FilePath(platformString: "") + XCTAssertEqual(path.string.isEmpty, true) + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_FilePath_initWithInoutConversion() { + var c: CInterop.PlatformChar = 0 + let path = FilePath(platformString: &c) + // Any other value of `c` would violate the null-terminated precondition + XCTAssertEqual(path.string.isEmpty, true) + } + + func test_FilePathComponent_initWithArrayConversion() { + var source: [CInterop.PlatformChar] = [0x61, 0x62, 0, 0x63] + var component: FilePath.Component? + component = FilePath.Component(platformString: source) + source.withUnsafeBufferPointer { + XCTAssertEqual(component, .init(platformString: $0.baseAddress!)) + } + source[1] = CInterop.PlatformChar(truncatingIfNeeded: 0xffff) + component = FilePath.Component(platformString: source) + source.withUnsafeBufferPointer { + XCTAssertEqual(component, .init(platformString: $0.baseAddress!)) + } + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_FilePathComponent_initWithStringConversion() { + let source = "ab\0c" + var component: FilePath.Component? + component = FilePath.Component(platformString: source) + source.withPlatformString { + XCTAssertEqual(component, FilePath.Component(platformString: $0)) + } + component = FilePath.Component(platformString: "") + XCTAssertNil(component) + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_FilePathComponent_initWithInoutConversion() { + var c: CInterop.PlatformChar = 0 + let component = FilePath.Component(platformString: &c) + XCTAssertNil(component) + } + + func test_FilePathRoot_initWithArrayConversion() { + let source: [CInterop.PlatformChar] + #if os(Windows) + source = [0x41, 0x3a, 0x5c, 0, 0x7f] + #else // unix + source = [0x2f, 0, 0x7f] + #endif + var root: FilePath.Root? + root = FilePath.Root(platformString: source) + source.withUnsafeBufferPointer { + XCTAssertEqual(root, FilePath.Root(platformString: $0.baseAddress!)) + } + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_FilePathRoot_initWithStringConversion() { + #if os(Windows) + let source = "C:\\\0 and the rest" + #else // unix + let source = "/\0 and the rest" + #endif + var root: FilePath.Root? + root = FilePath.Root(platformString: source) + source.withPlatformString { + XCTAssertEqual(root, FilePath.Root(platformString: $0)) + } + root = FilePath.Root(platformString: "") + XCTAssertNil(root) + } + + @available(*, deprecated) // silence the warning for using a deprecated api + func test_FilePathRoot_initWithInoutConversion() { + var c: CInterop.PlatformChar = 0 + let root = FilePath.Root(platformString: &c) + XCTAssertNil(root) + } +} diff --git a/Tests/SystemTests/TestingInfrastructure.swift b/Tests/SystemTests/TestingInfrastructure.swift index 6c088d47..12610ff9 100644 --- a/Tests/SystemTests/TestingInfrastructure.swift +++ b/Tests/SystemTests/TestingInfrastructure.swift @@ -8,8 +8,36 @@ */ import XCTest -import SystemInternals -import SystemPackage + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +internal struct Wildcard: Hashable {} + +#if ENABLE_MOCKING +extension Trace.Entry { + /// This implements `==` with wildcard matching. + /// (`Entry` cannot conform to `Equatable`/`Hashable` this way because + /// the wildcard matching `==` relation isn't transitive.) + internal func matches(_ other: Self) -> Bool { + guard self.name == other.name else { return false } + guard self.arguments.count == other.arguments.count else { return false } + for i in self.arguments.indices { + if self.arguments[i] is Wildcard || other.arguments[i] is Wildcard { + continue + } + guard self.arguments[i] == other.arguments[i] else { return false } + } + return true + } +} +#endif // ENABLE_MOCKING + +// To aid debugging, force failures to fatal error +internal let forceFatalFailures = false internal protocol TestCase { // TODO: want a source location stack, more fidelity, kinds of stack entries, etc @@ -18,21 +46,81 @@ internal protocol TestCase { // TODO: Instead have an attribute to register a test in a allTests var, similar to the argument parser. func runAllTests() + + // Customization hook: add adornment to reported failure reason + // Defaut: reason or empty + func failureMessage(_ reason: String?) -> String } + extension TestCase { + // Default implementation + func failureMessage(_ reason: String?) -> String { reason ?? "" } + func expectEqualSequence( - _ actual: S1, _ expected: S2, + _ expected: S1, _ actual: S2, _ message: String? = nil ) where S1.Element: Equatable, S1.Element == S2.Element { - if !actual.elementsEqual(expected) { + if !expected.elementsEqual(actual) { + defer { print("expected: \(expected)\n actual: \(actual)") } fail(message) } } func expectEqual( - _ actual: E, _ expected: E, + _ expected: E, _ actual: E, _ message: String? = nil ) { if actual != expected { + defer { print("expected: \(expected)\n actual: \(actual)") } + fail(message) + } + } + func expectNotEqual( + _ expected: E, _ actual: E, + _ message: String? = nil + ) { + if actual == expected { + defer { print("expected not equal: \(expected) and \(actual)") } + fail(message) + } + } + #if ENABLE_MOCKING + func expectMatch( + _ expected: Trace.Entry?, _ actual: Trace.Entry?, + _ message: String? = nil + ) { + func check() -> Bool { + switch (expected, actual) { + case let (expected?, actual?): + return expected.matches(actual) + case (nil, nil): + return true + default: + return false + } + } + if !check() { + let e = expected.map { "\($0)" } ?? "nil" + let a = actual.map { "\($0)" } ?? "nil" + defer { print("expected: \(e)\n actual: \(a)") } + fail(message) + } + } + #endif // ENABLE_MOCKING + func expectNil( + _ actual: T?, + _ message: String? = nil + ) { + if actual != nil { + defer { print("expected nil: \(actual!)") } + fail(message) + } + } + func expectNotNil( + _ actual: T?, + _ message: String? = nil + ) { + if actual == nil { + defer { print("expected non-nil") } fail(message) } } @@ -40,26 +128,44 @@ extension TestCase { _ actual: Bool, _ message: String? = nil ) { - expectEqual(true, actual, message) + if !actual { fail(message) } } func expectFalse( _ actual: Bool, _ message: String? = nil ) { - expectEqual(false, actual, message) + if actual { fail(message) } } func fail(_ reason: String? = nil) { - XCTAssert(false, reason ?? "", file: file, line: line) + XCTAssert(false, failureMessage(reason), file: file, line: line) + if forceFatalFailures { + fatalError(reason ?? "") + } } + } +#if ENABLE_MOCKING internal struct MockTestCase: TestCase { var file: StaticString var line: UInt var expected: Trace.Entry - var interruptable: Bool + var interruptBehavior: InterruptBehavior + + var interruptable: Bool { return interruptBehavior == .interruptable } + + internal enum InterruptBehavior { + // Retry the syscall on EINTR + case interruptable + + // Cannot return EINTR + case noInterrupt + + // Cannot error at all + case noError + } var body: (_ retryOnInterrupt: Bool) throws -> () @@ -67,14 +173,14 @@ internal struct MockTestCase: TestCase { _ file: StaticString = #file, _ line: UInt = #line, name: String, + _ interruptable: InterruptBehavior, _ args: AnyHashable..., - interruptable: Bool, - _ body: @escaping (_ retryOnInterrupt: Bool) throws -> () + body: @escaping (_ retryOnInterrupt: Bool) throws -> () ) { self.file = file self.line = line self.expected = Trace.Entry(name: name, args) - self.interruptable = interruptable + self.interruptBehavior = interruptable self.body = body } @@ -88,11 +194,24 @@ internal struct MockTestCase: TestCase { // Test our API mappings to the lower-level syscall invocation do { try body(true) - self.expectEqual(mocking.trace.dequeue(), self.expected) + self.expectMatch(self.expected, mocking.trace.dequeue()) } catch { self.fail() } + // Non-error-ing syscalls shouldn't ever throw + guard interruptBehavior != .noError else { + do { + try body(interruptable) + self.expectMatch(self.expected, mocking.trace.dequeue()) + try body(!interruptable) + self.expectMatch(self.expected, mocking.trace.dequeue()) + } catch { + self.fail() + } + return + } + // Test interupt behavior. Interruptable calls will be told not to // retry to catch the EINTR. Non-interruptable calls will be told to // retry, to make sure they don't spin (e.g. if API changes to include @@ -103,7 +222,7 @@ internal struct MockTestCase: TestCase { self.fail() } catch Errno.interrupted { // Success! - self.expectEqual(mocking.trace.dequeue(), self.expected) + self.expectMatch(self.expected, mocking.trace.dequeue()) } catch { self.fail() } @@ -114,16 +233,21 @@ internal struct MockTestCase: TestCase { mocking.forceErrno = .counted(errno: EINTR, count: 3) try body(interruptable) - self.expectEqual(mocking.trace.dequeue(), self.expected) // EINTR - self.expectEqual(mocking.trace.dequeue(), self.expected) // EINTR - self.expectEqual(mocking.trace.dequeue(), self.expected) // EINTR - self.expectEqual(mocking.trace.dequeue(), self.expected) // Success + self.expectMatch(self.expected, mocking.trace.dequeue()) // EINTR + self.expectMatch(self.expected, mocking.trace.dequeue()) // EINTR + self.expectMatch(self.expected, mocking.trace.dequeue()) // EINTR + self.expectMatch(self.expected, mocking.trace.dequeue()) // Success } catch Errno.interrupted { self.expectFalse(interruptable) - self.expectEqual(mocking.trace.dequeue(), self.expected) // EINTR + self.expectMatch(self.expected, mocking.trace.dequeue()) // EINTR } catch { self.fail() } } } } +#endif // ENABLE_MOCKING + +internal func withWindowsPaths(enabled: Bool, _ body: () -> ()) { + _withWindowsPaths(enabled: enabled, body) +} diff --git a/Tests/SystemTests/UtilTests.swift b/Tests/SystemTests/UtilTests.swift new file mode 100644 index 00000000..e9523207 --- /dev/null +++ b/Tests/SystemTests/UtilTests.swift @@ -0,0 +1,89 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 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 +*/ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +class UtilTests: XCTestCase { + func testStackBuffer() { + // Exercises _withStackBuffer a bit, in hopes any bugs will + // show up as ASan failures. + for size in stride(from: 0, to: 1000, by: 5) { + var called = false + _withStackBuffer(capacity: size) { buffer in + XCTAssertFalse(called) + called = true + + buffer.initializeMemory(as: UInt8.self, repeating: 42) + } + XCTAssertTrue(called) + } + } + + func testCStringArray() { + func check( + _ array: [String], + file: StaticString = #filePath, + line: UInt = #line + ) { + array._withCStringArray { carray in + let actual = carray.map { $0.map { String(cString: $0) } ?? "" } + XCTAssertEqual(actual, array, file: file, line: line) + // Verify that there is a null pointer following the last item in + // carray. (Note: this is intentionally addressing beyond the + // end of the buffer, as the function promises that is going to be okay.) + XCTAssertNil((carray.baseAddress! + carray.count).pointee) + } + } + + check([]) + check([""]) + check(["", ""]) + check(["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]) + // String literals of various sizes and counts + check(["hello"]) + check(["hello", "world"]) + check(["This is a rather large string literal"]) + check([ + "This is a rather large string literal", + "This is small", + "This one is not that small -- it's even longer than the first", + ]) + check([ + "This is a rather large string literal", + "This one is not that small -- it's even longer than the first", + "And this is the largest of them all. I wonder if it even fits on a line" + ]) + check(Array(repeating: "", count: 100)) + check(Array(repeating: "Hiii", count: 100)) + check(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"]) + check(["", "b", "", "d", "", "f", "", "h", "", "j", "", "👨‍👨‍👧‍👦👩‍❤️‍💋‍👨", "m"]) + + var girls = ["Dörothy", "Róse", "Blánche", "Sőphia"] + check(girls) // Small strings + for i in girls.indices { + // Convert to native + girls[i] = "\(girls[i]) \(girls[i]) \(girls[i])" + } + check(girls) // Native large strings + for i in girls.indices { + let data = girls[i].data(using: .utf16)! + girls[i] = NSString( + data: data, + encoding: String.Encoding.utf16.rawValue + )! as String + } + check(girls) // UTF-16 Cocoa strings + } +} diff --git a/Tests/SystemTests/XCTestManifests.swift b/Tests/SystemTests/XCTestManifests.swift deleted file mode 100644 index c40f26c1..00000000 --- a/Tests/SystemTests/XCTestManifests.swift +++ /dev/null @@ -1,71 +0,0 @@ -#if !canImport(ObjectiveC) -import XCTest - -extension ErrnoTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ErrnoTest = [ - ("testConstants", testConstants), - ("testPatternMatching", testPatternMatching), - ] -} - -extension FileDescriptorTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FileDescriptorTest = [ - ("testConstants", testConstants), - ] -} - -extension FileOperationsTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FileOperationsTest = [ - ("testAdHocOpen", testAdHocOpen), - ("testHelpers", testHelpers), - ("testSyscalls", testSyscalls), - ] -} - -extension FilePathTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FilePathTest = [ - ("testFilePath", testFilePath), - ] -} - -extension FilePermissionsTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FilePermissionsTest = [ - ("testPermissions", testPermissions), - ] -} - -extension MockingTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__MockingTest = [ - ("testMocking", testMocking), - ] -} - -public func __allTests() -> [XCTestCaseEntry] { - return [ - testCase(ErrnoTest.__allTests__ErrnoTest), - testCase(FileDescriptorTest.__allTests__FileDescriptorTest), - testCase(FileOperationsTest.__allTests__FileOperationsTest), - testCase(FilePathTest.__allTests__FilePathTest), - testCase(FilePermissionsTest.__allTests__FilePermissionsTest), - testCase(MockingTest.__allTests__MockingTest), - ] -} -#endif diff --git a/cmake/modules/CMakeLists.txt b/cmake/modules/CMakeLists.txt new file mode 100644 index 00000000..17f3f0b8 --- /dev/null +++ b/cmake/modules/CMakeLists.txt @@ -0,0 +1,19 @@ +#[[ +This source file is part of the Swift System open source Project + +Copyright (c) 2021 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 +#]] + +set(SWIFT_SYSTEM_EXPORTS_FILE ${CMAKE_CURRENT_BINARY_DIR}/SwiftSystemExports.cmake) + +configure_file(SwiftSystemConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/SwiftSystemConfig.cmake) + +get_property(SWIFT_SYSTEM_EXPORTS GLOBAL PROPERTY SWIFT_SYSTEM_EXPORTS) +export(TARGETS ${SWIFT_SYSTEM_EXPORTS} + NAMESPACE SwiftSystem:: + FILE ${SWIFT_SYSTEM_EXPORTS_FILE} + EXPORT_LINK_INTERFACE_LIBRARIES) diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake new file mode 100644 index 00000000..5fb831a9 --- /dev/null +++ b/cmake/modules/SwiftSupport.cmake @@ -0,0 +1,120 @@ +#[[ +This source file is part of the Swift System open source project + +Copyright (c) 2020 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 +#]] + +# Returns the architecture name in a variable +# +# Usage: +# get_swift_host_arch(result_var_name) +# +# Sets ${result_var_name} with the converted architecture name derived from +# CMAKE_SYSTEM_PROCESSOR. +function(get_swift_host_arch result_var_name) + if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64") + set("${result_var_name}" "x86_64" PARENT_SCOPE) + elseif ("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "AArch64|aarch64|arm64|ARM64") + if(CMAKE_SYSTEM_NAME MATCHES Darwin) + set("${result_var_name}" "arm64" PARENT_SCOPE) + else() + set("${result_var_name}" "aarch64" PARENT_SCOPE) + endif() + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64") + set("${result_var_name}" "powerpc64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64le") + set("${result_var_name}" "powerpc64le" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "s390x") + set("${result_var_name}" "s390x" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv6l") + set("${result_var_name}" "armv6" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7l") + set("${result_var_name}" "armv7" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7-a") + set("${result_var_name}" "armv7" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "amd64") + set("${result_var_name}" "x86_64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64") + set("${result_var_name}" "x86_64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "IA64") + set("${result_var_name}" "itanium" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86") + set("${result_var_name}" "i686" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "i686") + set("${result_var_name}" "i686" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "riscv64") + set("${result_var_name}" "riscv64" PARENT_SCOPE) + else() + message(FATAL_ERROR "Unrecognized architecture on host system: ${CMAKE_SYSTEM_PROCESSOR}") + endif() +endfunction() + +# Returns the os name in a variable +# +# Usage: +# get_swift_host_os(result_var_name) +# +# +# Sets ${result_var_name} with the converted OS name derived from +# CMAKE_SYSTEM_NAME. +function(get_swift_host_os result_var_name) + if(CMAKE_SYSTEM_NAME STREQUAL Darwin) + set(${result_var_name} macosx PARENT_SCOPE) + else() + string(TOLOWER ${CMAKE_SYSTEM_NAME} cmake_system_name_lc) + set(${result_var_name} ${cmake_system_name_lc} PARENT_SCOPE) + endif() +endfunction() + +if(NOT Swift_MODULE_TRIPLE) + # Attempt to get the module triple from the Swift compiler. + set(module_triple_command "${CMAKE_Swift_COMPILER}" -print-target-info) + if(CMAKE_Swift_COMPILER_TARGET) + list(APPEND module_triple_command -target ${CMAKE_Swift_COMPILER_TARGET}) + endif() + execute_process(COMMAND ${module_triple_command} + OUTPUT_VARIABLE target_info_json) + string(JSON module_triple GET "${target_info_json}" "target" "moduleTriple") + + # Exit now if we failed to infer the triple. + if(NOT module_triple) + message(FATAL_ERROR + "Failed to get module triple from Swift compiler. " + "Compiler output: ${target_info_json}") + endif() + + # Cache the module triple for future use. + set(Swift_MODULE_TRIPLE "${module_triple}" CACHE STRING "swift module triple used for installed swiftmodule and swiftinterface files") + mark_as_advanced(Swift_MODULE_TRIPLE) +endif() + +function(_install_target module) + get_swift_host_os(swift_os) + get_target_property(type ${module} TYPE) + + if(type STREQUAL STATIC_LIBRARY) + set(swift swift_static) + else() + set(swift swift) + endif() + + install(TARGETS ${module}) + if(type STREQUAL EXECUTABLE) + return() + endif() + + get_target_property(module_name ${module} Swift_MODULE_NAME) + if(NOT module_name) + set(module_name ${module}) + endif() + + install(FILES $/${module_name}.swiftdoc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/${swift}/${swift_os}/${module_name}.swiftmodule + RENAME ${Swift_MODULE_TRIPLE}.swiftdoc) + install(FILES $/${module_name}.swiftmodule + DESTINATION ${CMAKE_INSTALL_LIBDIR}/${swift}/${swift_os}/${module_name}.swiftmodule + RENAME ${Swift_MODULE_TRIPLE}.swiftmodule) +endfunction() diff --git a/cmake/modules/SwiftSystemConfig.cmake.in b/cmake/modules/SwiftSystemConfig.cmake.in new file mode 100644 index 00000000..321d9389 --- /dev/null +++ b/cmake/modules/SwiftSystemConfig.cmake.in @@ -0,0 +1,12 @@ +#[[ +This source file is part of the Swift System open source Project + +Copyright (c) 2021 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(NOT TARGET SystemPackage) + include("@SWIFT_SYSTEM_EXPORTS_FILE@") +endif()