From e938d94dbd29b5397d349ede851073e8d214f0c8 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld <1004103+jmschonfeld@users.noreply.github.com> Date: Fri, 26 Jul 2024 13:14:08 -0700 Subject: [PATCH 01/34] Correctly set .fileTypeSymlink on windows (#788) --- .../FileManager/FileManager+Files.swift | 14 ++++++++++++-- .../FileManager/FileManagerTests.swift | 2 -- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index f8a770026..72e05f937 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -555,7 +555,7 @@ extension _FileManagerImpl { func attributesOfItem(atPath path: String) throws -> [FileAttributeKey : Any] { #if os(Windows) return try path.withNTPathRepresentation { pwszPath in - let hFile = CreateFileW(pwszPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nil) + let hFile = CreateFileW(pwszPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nil) if hFile == INVALID_HANDLE_VALUE { throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true) } @@ -567,7 +567,7 @@ extension _FileManagerImpl { } let dwFileType = GetFileType(hFile) - let fatType: FileAttributeType = switch (dwFileType) { + var fatType: FileAttributeType = switch (dwFileType) { case FILE_TYPE_CHAR: FileAttributeType.typeCharacterSpecial case FILE_TYPE_DISK: info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY @@ -578,6 +578,16 @@ extension _FileManagerImpl { default: FileAttributeType.typeUnknown } + if info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT { + // This could by a symlink, check if that's the case and update fatType if necessary + var tagInfo = FILE_ATTRIBUTE_TAG_INFO() + if GetFileInformationByHandleEx(hFile, FileAttributeTagInfo, &tagInfo, DWORD(MemoryLayout.size)) { + if tagInfo.ReparseTag == IO_REPARSE_TAG_SYMLINK { + fatType = .typeSymbolicLink + } + } + } + let systemNumber = UInt64(info.dwVolumeSerialNumber) let systemFileNumber = UInt64(info.nFileIndexHigh << 32) | UInt64(info.nFileIndexLow) let referenceCount = UInt64(info.nNumberOfLinks) diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 84f180a83..0cae49739 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -890,13 +890,11 @@ final class FileManagerTests : XCTestCase { XCTAssertEqual(attrs[.type] as? FileAttributeType, FileAttributeType.typeDirectory) } - #if !os(Windows) do { try $0.createSymbolicLink(atPath: "symlink", withDestinationPath: "file") let attrs = try $0.attributesOfItem(atPath: "symlink") XCTAssertEqual(attrs[.type] as? FileAttributeType, FileAttributeType.typeSymbolicLink) } - #endif } } } From dea1d79898443c14fd49e3e376f97c329a7a8f9f Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Sat, 27 Jul 2024 10:44:27 -0700 Subject: [PATCH 02/34] Update README (#796) (#797) (cherry picked from commit 604de76fd4280c86666a2d6cb3a8f704d7336ecd) --- Foundation_Build_Process.md | 13 +++++- README.md | 89 +++++++++++++------------------------ 2 files changed, 43 insertions(+), 59 deletions(-) diff --git a/Foundation_Build_Process.md b/Foundation_Build_Process.md index 93cc678ad..155b39fb8 100644 --- a/Foundation_Build_Process.md +++ b/Foundation_Build_Process.md @@ -116,4 +116,15 @@ Dependencies are managed by the `utils/update-checkout` script. This will check ## `FOUNDATION_FRAMEWORK` Build -The swift-foundation project is also built internally within Apple as part of the `Foundation.framework` library that is installed into the OS of all Apple platforms. This is a special build configuration with the `FOUNDATION_FRAMEWORK` condition defined that is not built via open source CI. Code within this condition is only relevant when building swift-foundation as part of `Foundation.framework` and is not used in any open source builds of Swift. Note that this does not apply to swift-foundation-icu (which is built differently internally) or swift-corelibs-foundation (which is not built for Darwin platforms). \ No newline at end of file +The swift-foundation project is also built internally within Apple as part of the `Foundation.framework` library that is installed into the OS of all Apple platforms. This is a special build configuration with the `FOUNDATION_FRAMEWORK` condition defined that is not built via open source CI. Code within this condition is only relevant when building swift-foundation as part of `Foundation.framework` and is not used in any open source builds of Swift. Note that this does not apply to swift-foundation-icu (which is built differently internally) or swift-corelibs-foundation (which is not built for Darwin platforms). + +## Benchmarks + +Benchmarks for `swift-foundation` are in a separate Swift Package in the `Benchmarks` subfolder of this repository. +They use the [`package-benchmark`](https://github.com/ordo-one/package-benchmark) plugin. +Benchmarks depends on the [`jemalloc`](https://jemalloc.net) memory allocation library, which is used by `package-benchmark` to capture memory allocation statistics. +An installation guide can be found in the [Getting Started article](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/gettingstarted#Installing-Prerequisites-and-Platform-Support) of `package-benchmark`. +Afterwards you can run the benchmarks from CLI by going to the `Benchmarks` subfolder (e.g. `cd Benchmarks`) and invoking: +``` +swift package benchmark +``` \ No newline at end of file diff --git a/README.md b/README.md index 33520488a..17f33539b 100644 --- a/README.md +++ b/README.md @@ -9,91 +9,64 @@ It is designed with these goals in mind: * Demonstrate useful conventions that can be widely adopted by the Swift ecosystem * Support internationalization and localization to make software accessible around the world -## Current State +This project, `swift-foundation`, provides a shared implementation of key Foundation API for all platforms. -This package is a work in progress that aims to build a new and unified Swift implementation of Foundation for all platforms. +On macOS, iOS, and other Apple platforms, apps should use the Foundation that comes with the operating system. The Foundation framework includes this code. -It is in its early stages with many features still to be implemented. - -The following types are available, with more to come later: - -* **FoundationEssentials** - * `AttributedString` - * `Data` - * `Date` - * `DateInterval` - * `JSONEncoder` - * `JSONDecoder` - * `Predicate` - * `String` extensions - * `UUID` -* **Internationalization** - * `Calendar` - * `TimeZone` - * `Locale` - * `DateComponents` - * `FormatStyle` - * `ParseStrategy` - -Many types, including `JSONEncoder`, `Calendar`, `TimeZone`, and `Locale` are all-new Swift implementations. `FormatStyle` and `ParseStrategy` available as open source for the first time. - -For internationalization support on non-Darwin platforms, we created a separate package named *[FoundationICU](https://github.com/apple/swift-foundation-icu)*. This repository contains the necessary ICU implementations and data from the upstream [Apple OSS Distribution ICU](https://github.com/apple-oss-distributions/ICU), wrapped in Swift so FoundationInternationalization can easily depend on it. - -Using a common version of ICU will result in more reliable and consistent results when formatting dates, times, and numbers. -### Development Focus for 2023 - -Quality and performance are our two most important goals for the project. Therefore, the plans for the first half of 2023 are continuing refinement of the core API, adding to our suites of unit and performance tests, and expanding to other platforms where possible, using the most relevant code from [swift-corelibs-foundation](https://github.com/apple/swift-corelibs-foundation). - -Later this year, the porting effort will continue. It will bring high quality Swift implementations of additional important Foundation API such as `URL`, `Bundle`, `FileManager`, `FileHandle`, `Process`, `SortDescriptor`, `SortComparator` and more. +On all other Swift platforms, `swift-foundation` is available as part of the toolchain. Simply `import FoundationEssentials` or `import FoundationInternationalization` to use its API. It is also re-exported from [swift-corelibs-foundation](http://github.com/apple/swift-corelibs-foundation)'s `Foundation`, `FoundationXML`, and `FoundationNetworking` modules. ## Building and Testing > [!NOTE] > Building swift-foundation requires the in-development Swift 6.0 toolchain. You can download the Swift 6.0 nightly toolchain from [the Swift website](https://swift.org/install). -Before building Foundation, first ensure that you have Swift installed on your device. Once you have a Swift toolchain installed, check out the _Getting Started_ section of the [Foundation Build Process](Foundation_Build_Process.md#getting-started) guide for steps to build Foundation. +Before building Foundation, first ensure that you have a Swift toolchain installed. Next, check out the _Getting Started_ section of the [Foundation Build Process](Foundation_Build_Process.md#getting-started) guide for detailed steps on building and testing. -## Performance -Being written in Swift, this new implementation provides some major benefits over the previous C and Objective-C versions. +## Project Navigator -`Locale`, `TimeZone` and `Calendar` no longer require bridging from Objective-C. Common tasks like getting a fixed `Locale` are an order of magnitude faster for Swift clients. `Calendar`'s ability to calculate important dates can take better advantage of Swift’s value semantics to avoid intermediate allocations, resulting in over a 20% improvement in some benchmarks. Date formatting using `FormatStyle` also has some major performance upgrades, showing a massive 150% improvement in a benchmark of formatting with a standard date and time template. +Foundation builds in different configurations and is composed of several projects. -Even more exciting are the improvements to JSON decoding in the new package. Foundation has a brand-new Swift implementation for `JSONDecoder` and `JSONEncoder`, eliminating costly roundtrips to and from the Objective-C collection types. The tight integration of parsing JSON in Swift for initializing `Codable` types improves performance, too. In benchmarks parsing [test data](https://www.boost.org/doc/libs/master/libs/json/doc/html/json/benchmarks.html), there are improvements in decode time from 200% to almost 500%. +```mermaid + graph TD; + FF[Foundation.framework]-->SF + subgraph GitHub + SCLF[swift-corelibs-foundation]-->SF + SF[swift-foundation]-->FICU[swift-foundation-icu] + SF-->SC[swift-collections] + end +``` -### Benchmarks +### Swift Foundation -Benchmarks for `swift-foundation` are in a separate Swift Package in the `Benchmarks` subfolder of this repository. -They use the [`package-benchmark`](https://github.com/ordo-one/package-benchmark) plugin. -Benchmarks depends on the [`jemalloc`](https://jemalloc.net) memory allocation library, which is used by `package-benchmark` to capture memory allocation statistics. -An installation guide can be found in the [Getting Started article](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/gettingstarted#Installing-Prerequisites-and-Platform-Support) of `package-benchmark`. -Afterwards you can run the benchmarks from CLI by going to the `Benchmarks` subfolder (e.g. `cd Benchmarks`) and invoking: -``` -swift package benchmark -``` +A shared library shipped in the Swift toolchain, written in Swift. It provides the core implementation of many key types, including `URL`, `Data`, `JSONDecoder`, `Locale`, `Calendar`, and more in the `FoundationEssentials` and `FoundationInternationalization` modules. Its source code is shared across all platforms. -## Governance +_swift-foundation_ depends on a limited set of packages, primarily [swift-collections](http://github.com/apple/swift-collections) and [swift-syntax](http://github.com/apple/swift-syntax). -The success of the Swift language is an example of what's possible when a community comes together with a shared interest. +### Swift Corelibs Foundation -For Foundation, our goal is to create the best fundamental data types and internationalization features, and make them available to Swift developers everywhere. It will take advantage of emerging features in the language as they are added, and enable library and app authors to build higher level API with confidence. +A shared library shipped in the Swift toolchain. It provides compatibility API for clients that need pre-Swift API from Foundation. It is written in Swift and C. It provides, among other types, `NSObject`, class-based data structures, `NSFormatter`, and `NSKeyedArchiver`. It re-exports the `FoundationEssentials` and `FoundationInternationalization` modules, allowing compatibility for source written before the introduction of the _swift-foundation_ project. As these implementations are distinct from those written in Objective-C, the compatibility is best-effort only. -Moving Foundation into this future requires not only an improved implementation, but also an improved process for using it outside of Apple’s platforms. Therefore, Foundation now has a path for the community to add new API for the benefit of Swift developers on every platform. +[swift-corelibs-foundation](http://github.com/apple/swift-corelibs-foundation) builds for non-Darwin platforms only. It installs the `Foundation` umbrella module, `FoundationXML`, and `FoundationNetworking`. -The Foundation package is an independent project in its early incubation stages. Inspired by the workgroups in the Swift project, it has a workgroup to (a) oversee [community API proposals](Evolution.md) and (b) to closely coordinate with developments in the Swift project and Apple platforms. In the future, we will explore how to sunset the existing [swift-corelibs-foundation](https://github.com/apple/swift-corelibs-foundation) and migrate to using the new version of Foundation created by this project. +### Foundation ICU -The workgroup meets regularly to review proposals, look at emerging trends in the Swift ecosystem, and discuss how the library can evolve to best meet our common goals. +A private library for Foundation, wrapping ICU. Using a standard version of ICU provides stability in the behavior of our internationalization API, and consistency with the latest releases on Darwin platforms. It is imported from the `FoundationInternationalization` module only. Clients that do not need API that relies upon the data provided by ICU can import `FoundationEssentials` instead. -## Foundation Framework and Foundation Package +### Foundation Framework + +A [framework](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Frameworks.html) built into macOS, iOS, and all other Darwin platforms. It is written in a combination of C, Objective-C, and Swift. The Foundation framework compiles the sources from _swift-foundation_ into its binary and provides one `Foundation` module that contains all features. + +## Governance -The Swift code in the package is the core of the Foundation framework that ships on macOS, iOS, and other Apple platforms. As new Swift implementations of Foundation API are implemented in the package, Apple will use those implementations in the framework as well. +Foundation's goal is to create the best fundamental data types and internationalization features, and make them available to Swift developers everywhere. It takes advantage of emerging features in the language as they are added, and enables library and app authors to build higher level API with confidence. -The Foundation framework may have the occasional need to add Darwin-specific API, but our goal is to share as much code and API between all platforms as possible. In cases where platform-specific code is needed within a single source file, a compiler directive is used to include or exclude it. +This project is part of the overall [Swift project](https://swift.org). It has a workgroup to (a) oversee [community API proposals](Evolution.md) and (b) to closely coordinate with developments in the Swift project and Apple platforms. The workgroup meets regularly to review proposals, look at emerging trends in the Swift ecosystem, and discuss how the library should evolve. ## Contributions Foundation welcomes contributions from the community, including bug fixes, tests, documentation, and ports to new platforms. -The project uses the [Swift forums for discussion](https://forums.swift.org/c/related-projects/foundation/99) and [GitHub Issues](https://github.com/apple/swift-foundation/issues) for tracking bugs, feature requests, and other work. +We use the [Swift forums for discussion](https://forums.swift.org/c/related-projects/foundation/99) and [GitHub Issues](https://github.com/apple/swift-foundation/issues) for tracking bugs, feature requests, and other work. Please see the [CONTRIBUTING](https://github.com/apple/swift-foundation/blob/main/CONTRIBUTING.md) document for more information, including the process for accepting community contributions for new API in Foundation. From d25d97ba48a42bda4218b38a1ee970bf69db3849 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Sat, 27 Jul 2024 17:52:15 -0700 Subject: [PATCH 03/34] (132587065) URL: Don't standardize relative file paths (#792) (#798) (cherry picked from commit e1223a03f54f72d2f0aa4a4653f40c82534bb2c0) Co-authored-by: Jonathan Flat <50605158+jrflat@users.noreply.github.com> --- Sources/FoundationEssentials/URL/URL.swift | 8 -------- Tests/FoundationEssentialsTests/URLTests.swift | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index d23855889..56a88db60 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -2155,14 +2155,6 @@ extension URL { isDirectory = filePath.utf8.last == slash } - if !isAbsolute { - #if !NO_FILESYSTEM - filePath = filePath.standardizingPath - #else - filePath = filePath.removingDotSegments - #endif - } - #if os(Windows) // Convert any "\" back to "/" before storing the URL parse info filePath = filePath.replacing(UInt8(ascii: "\\"), with: UInt8(ascii: "/")) diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 6366f1d2e..a1f7c6b49 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -352,6 +352,23 @@ final class URLTests : XCTestCase { } } + func testURLRelativeDotDotResolution() throws { + let baseURL = URL(filePath: "/docs/src/") + var result = URL(filePath: "../images/foo.png", relativeTo: baseURL) + #if FOUNDATION_FRAMEWORK_NSURL + XCTAssertEqual(result.path, "/docs/images/foo.png") + #else + XCTAssertEqual(result.path(), "/docs/images/foo.png") + #endif + + result = URL(filePath: "/../images/foo.png", relativeTo: baseURL) + #if FOUNDATION_FRAMEWORK_NSURL + XCTAssertEqual(result.path, "/../images/foo.png") + #else + XCTAssertEqual(result.path(), "/../images/foo.png") + #endif + } + func testAppendFamily() throws { let base = URL(string: "https://www.example.com")! From f7ce4d5979270cb729c01b21fce25b5c18690c78 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld <1004103+jmschonfeld@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:01:48 -0700 Subject: [PATCH 04/34] Add explicit modulemap path to take priority over modulemap from SDK (#789) (#800) --- Sources/_FoundationCShims/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/_FoundationCShims/CMakeLists.txt b/Sources/_FoundationCShims/CMakeLists.txt index ef2a7fe22..1798a5bb0 100644 --- a/Sources/_FoundationCShims/CMakeLists.txt +++ b/Sources/_FoundationCShims/CMakeLists.txt @@ -19,6 +19,9 @@ add_library(_FoundationCShims STATIC target_include_directories(_FoundationCShims PUBLIC include) +target_compile_options(_FoundationCShims INTERFACE + "$<$:SHELL:-Xcc -fmodule-map-file=${CMAKE_CURRENT_SOURCE_DIR}/include/module.modulemap>") + set_property(GLOBAL APPEND PROPERTY SWIFT_FOUNDATION_EXPORTS _FoundationCShims) if(BUILD_SHARED_LIBS) From ebd0f42c7916be3120ffd8c17d3bf6df549ad1d0 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld <1004103+jmschonfeld@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:02:35 -0700 Subject: [PATCH 05/34] [CMake] Fix macro install rpath (#793) (#803) --- Sources/FoundationMacros/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationMacros/CMakeLists.txt b/Sources/FoundationMacros/CMakeLists.txt index 0cc31c299..c95c5419e 100644 --- a/Sources/FoundationMacros/CMakeLists.txt +++ b/Sources/FoundationMacros/CMakeLists.txt @@ -43,8 +43,9 @@ target_link_libraries(FoundationMacros PUBLIC SwiftSyntax::SwiftSyntaxBuilder ) +# The macro is installed into lib/swift/host/plugins, but needs to load libraries from lib/swift/host and lib/swift/${SWIFT_SYSTEM_NAME} set_target_properties(FoundationMacros PROPERTIES - INSTALL_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN/../../../swift/${SWIFT_SYSTEM_NAME}:$ORIGIN/.." INSTALL_REMOVE_ENVIRONMENT_RPATH ON) target_compile_options(FoundationMacros PRIVATE -parse-as-library) From 4d1dfb4b6c0c3f3bfe9a88137e093f656a1da5b8 Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Thu, 1 Aug 2024 15:34:55 -0400 Subject: [PATCH 06/34] [6.0] Int128 support in json (#791) * Bare minimum [U]Int128 support for JSON encode/decode This doesn't yet tackle _slowpath_unwrapFixedWidthInteger, but at least allows us to round-trip [U]Int128 with our own encoder and decoder * Switch to if #available check for Int128 tests Also added some slow-path testing for [U]Int128, to ensure that we throw an error as expected in these cases instead of returning an incorrect value. --- .../JSON/JSONDecoder.swift | 30 ++++++ .../JSON/JSONEncoder.swift | 32 +++++++ .../JSONEncoderTests.swift | 91 +++++++++++++++++++ 3 files changed, 153 insertions(+) diff --git a/Sources/FoundationEssentials/JSON/JSONDecoder.swift b/Sources/FoundationEssentials/JSON/JSONDecoder.swift index 2b709d0ed..0565e0f8d 100644 --- a/Sources/FoundationEssentials/JSON/JSONDecoder.swift +++ b/Sources/FoundationEssentials/JSON/JSONDecoder.swift @@ -1128,6 +1128,11 @@ extension JSONDecoderImpl : SingleValueDecodingContainer { func decode(_: Int64.Type) throws -> Int64 { try decodeFixedWidthInteger() } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func decode(_: Int128.Type) throws -> Int128 { + try decodeFixedWidthInteger() + } func decode(_: UInt.Type) throws -> UInt { try decodeFixedWidthInteger() @@ -1148,6 +1153,11 @@ extension JSONDecoderImpl : SingleValueDecodingContainer { func decode(_: UInt64.Type) throws -> UInt64 { try decodeFixedWidthInteger() } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func decode(_: UInt128.Type) throws -> UInt128 { + try decodeFixedWidthInteger() + } func decode(_ type: T.Type) throws -> T { try self.unwrap(self.topValue, as: type, for: codingPathNode, _CodingKey?.none) @@ -1274,6 +1284,11 @@ extension JSONDecoderImpl { func decode(_: Int64.Type, forKey key: K) throws -> Int64 { try decodeFixedWidthInteger(key: key) } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func decode(_: Int128.Type, forKey key: K) throws -> Int128 { + try decodeFixedWidthInteger(key: key) + } func decode(_: UInt.Type, forKey key: K) throws -> UInt { try decodeFixedWidthInteger(key: key) @@ -1294,6 +1309,11 @@ extension JSONDecoderImpl { func decode(_: UInt64.Type, forKey key: K) throws -> UInt64 { try decodeFixedWidthInteger(key: key) } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func decode(_: UInt128.Type, forKey key: K) throws -> UInt128 { + try decodeFixedWidthInteger(key: key) + } func decode(_ type: T.Type, forKey key: K) throws -> T { try self.impl.unwrap(try getValue(forKey: key), as: type, for: codingPathNode, key) @@ -1456,6 +1476,11 @@ extension JSONDecoderImpl { mutating func decode(_: Int64.Type) throws -> Int64 { try decodeFixedWidthInteger() } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func decode(_: Int128.Type) throws -> Int128 { + try decodeFixedWidthInteger() + } mutating func decode(_: UInt.Type) throws -> UInt { try decodeFixedWidthInteger() @@ -1476,6 +1501,11 @@ extension JSONDecoderImpl { mutating func decode(_: UInt64.Type) throws -> UInt64 { try decodeFixedWidthInteger() } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func decode(_: UInt128.Type) throws -> UInt128 { + try decodeFixedWidthInteger() + } mutating func decode(_ type: T.Type) throws -> T { let value = try self.peekNextValue(ofType: type) diff --git a/Sources/FoundationEssentials/JSON/JSONEncoder.swift b/Sources/FoundationEssentials/JSON/JSONEncoder.swift index 9af6a1ca7..8ff91c4c9 100644 --- a/Sources/FoundationEssentials/JSON/JSONEncoder.swift +++ b/Sources/FoundationEssentials/JSON/JSONEncoder.swift @@ -712,6 +712,10 @@ private struct _JSONKeyedEncodingContainer : KeyedEncodingContain public mutating func encode(_ value: Int64, forKey key: Key) throws { reference.insert(self.encoder.wrap(value), for: _converted(key)) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + public mutating func encode(_ value: Int128, forKey key: Key) throws { + reference.insert(self.encoder.wrap(value), for: _converted(key)) + } public mutating func encode(_ value: UInt, forKey key: Key) throws { reference.insert(self.encoder.wrap(value), for: _converted(key)) } @@ -727,6 +731,10 @@ private struct _JSONKeyedEncodingContainer : KeyedEncodingContain public mutating func encode(_ value: UInt64, forKey key: Key) throws { reference.insert(self.encoder.wrap(value), for: _converted(key)) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + public mutating func encode(_ value: UInt128, forKey key: Key) throws { + reference.insert(self.encoder.wrap(value), for: _converted(key)) + } public mutating func encode(_ value: String, forKey key: Key) throws { reference.insert(self.encoder.wrap(value), for: _converted(key)) } @@ -827,11 +835,15 @@ private struct _JSONUnkeyedEncodingContainer : UnkeyedEncodingContainer { public mutating func encode(_ value: Int16) throws { self.reference.insert(self.encoder.wrap(value)) } public mutating func encode(_ value: Int32) throws { self.reference.insert(self.encoder.wrap(value)) } public mutating func encode(_ value: Int64) throws { self.reference.insert(self.encoder.wrap(value)) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + public mutating func encode(_ value: Int128) throws { self.reference.insert(self.encoder.wrap(value)) } public mutating func encode(_ value: UInt) throws { self.reference.insert(self.encoder.wrap(value)) } public mutating func encode(_ value: UInt8) throws { self.reference.insert(self.encoder.wrap(value)) } public mutating func encode(_ value: UInt16) throws { self.reference.insert(self.encoder.wrap(value)) } public mutating func encode(_ value: UInt32) throws { self.reference.insert(self.encoder.wrap(value)) } public mutating func encode(_ value: UInt64) throws { self.reference.insert(self.encoder.wrap(value)) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + public mutating func encode(_ value: UInt128) throws { self.reference.insert(self.encoder.wrap(value)) } public mutating func encode(_ value: String) throws { self.reference.insert(self.encoder.wrap(value)) } public mutating func encode(_ value: Float) throws { @@ -908,6 +920,12 @@ extension __JSONEncoder : SingleValueEncodingContainer { assertCanEncodeNewValue() self.storage.push(ref: wrap(value)) } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + public func encode(_ value: Int128) throws { + assertCanEncodeNewValue() + self.storage.push(ref: wrap(value)) + } public func encode(_ value: UInt) throws { assertCanEncodeNewValue() @@ -933,6 +951,12 @@ extension __JSONEncoder : SingleValueEncodingContainer { assertCanEncodeNewValue() self.storage.push(ref: wrap(value)) } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + public func encode(_ value: UInt128) throws { + assertCanEncodeNewValue() + self.storage.push(ref: wrap(value)) + } public func encode(_ value: String) throws { assertCanEncodeNewValue() @@ -967,11 +991,15 @@ private extension __JSONEncoder { @inline(__always) func wrap(_ value: Int16) -> JSONReference { .number(from: value) } @inline(__always) func wrap(_ value: Int32) -> JSONReference { .number(from: value) } @inline(__always) func wrap(_ value: Int64) -> JSONReference { .number(from: value) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + @inline(__always) func wrap(_ value: Int128) -> JSONReference { .number(from: value) } @inline(__always) func wrap(_ value: UInt) -> JSONReference { .number(from: value) } @inline(__always) func wrap(_ value: UInt8) -> JSONReference { .number(from: value) } @inline(__always) func wrap(_ value: UInt16) -> JSONReference { .number(from: value) } @inline(__always) func wrap(_ value: UInt32) -> JSONReference { .number(from: value) } @inline(__always) func wrap(_ value: UInt64) -> JSONReference { .number(from: value) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + @inline(__always) func wrap(_ value: UInt128) -> JSONReference { .number(from: value) } @inline(__always) func wrap(_ value: String) -> JSONReference { .string(value) } @inline(__always) @@ -1302,11 +1330,15 @@ extension Int8 : _JSONSimpleValueArrayElement { } extension Int16 : _JSONSimpleValueArrayElement { } extension Int32 : _JSONSimpleValueArrayElement { } extension Int64 : _JSONSimpleValueArrayElement { } +@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +extension Int128 : _JSONSimpleValueArrayElement { } extension UInt : _JSONSimpleValueArrayElement { } extension UInt8 : _JSONSimpleValueArrayElement { } extension UInt16 : _JSONSimpleValueArrayElement { } extension UInt32 : _JSONSimpleValueArrayElement { } extension UInt64 : _JSONSimpleValueArrayElement { } +@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +extension UInt128 : _JSONSimpleValueArrayElement { } extension String: _JSONSimpleValueArrayElement { fileprivate func jsonRepresentation(options: JSONEncoder._Options) -> String { self.serializedForJSON(withoutEscapingSlashes: options.outputFormatting.contains(.withoutEscapingSlashes)) diff --git a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift index 4fd275d7b..ee192fd6e 100644 --- a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift +++ b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift @@ -612,6 +612,9 @@ final class JSONEncoderTests : XCTestCase { _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt16], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt32], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt64], as: [Bool].self) + if #available(macOS 15.0, *) { + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt128], as: [Bool].self) + } _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Float], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self) } @@ -1323,6 +1326,94 @@ final class JSONEncoderTests : XCTestCase { let testValue = Numbers(floats: [.greatestFiniteMagnitude, .leastNormalMagnitude], doubles: [.greatestFiniteMagnitude, .leastNormalMagnitude]) _testRoundTrip(of: testValue) } + + func test_roundTrippingInt128() { + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + let values = [ + Int128.min, + Int128.min + 1, + -0x1_0000_0000_0000_0000, + 0x0_8000_0000_0000_0000, + -1, + 0, + 0x7fff_ffff_ffff_ffff, + 0x8000_0000_0000_0000, + 0xffff_ffff_ffff_ffff, + 0x1_0000_0000_0000_0000, + .max + ] + for i128 in values { + _testRoundTrip(of: i128) + } + } + } + + func test_Int128SlowPath() { + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + let decoder = JSONDecoder() + let work: [Int128] = [18446744073709551615, -18446744073709551615] + for value in work { + // force the slow-path by appending ".0" + let json = "\(value).0".data(using: String._Encoding.utf8)! + XCTAssertEqual(value, try? decoder.decode(Int128.self, from: json)) + } + // These should work, but making them do so probably requires + // rewriting the slow path to use a dedicated parser. For now, + // we ensure that they throw instead of returning some bogus + // result. + let shouldWorkButDontYet: [Int128] = [ + .min, -18446744073709551616, 18446744073709551616, .max + ] + for value in shouldWorkButDontYet { + // force the slow-path by appending ".0" + let json = "\(value).0".data(using: String._Encoding.utf8)! + XCTAssertThrowsError(try decoder.decode(Int128.self, from: json)) + } + } + } + + func test_roundTrippingUInt128() { + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + let values = [ + UInt128.zero, + 1, + 0x0000_0000_0000_0000_7fff_ffff_ffff_ffff, + 0x0000_0000_0000_0000_8000_0000_0000_0000, + 0x0000_0000_0000_0000_ffff_ffff_ffff_ffff, + 0x0000_0000_0000_0001_0000_0000_0000_0000, + 0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_ffff, + 0x8000_0000_0000_0000_0000_0000_0000_0000, + .max + ] + for u128 in values { + _testRoundTrip(of: u128) + } + } + } + + func test_UInt128SlowPath() { + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + let decoder = JSONDecoder() + let work: [UInt128] = [18446744073709551615] + for value in work { + // force the slow-path by appending ".0" + let json = "\(value).0".data(using: String._Encoding.utf8)! + XCTAssertEqual(value, try? decoder.decode(UInt128.self, from: json)) + } + // These should work, but making them do so probably requires + // rewriting the slow path to use a dedicated parser. For now, + // we ensure that they throw instead of returning some bogus + // result. + let shouldWorkButDontYet: [UInt128] = [ + 18446744073709551616, .max + ] + for value in shouldWorkButDontYet { + // force the slow-path by appending ".0" + let json = "\(value).0".data(using: String._Encoding.utf8)! + XCTAssertThrowsError(try decoder.decode(UInt128.self, from: json)) + } + } + } func test_roundTrippingDoubleValues() { struct Numbers : Codable, Equatable { From 40ca820694c5796b3e5e940b7c4146a0f59c4b0c Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:56:56 -0700 Subject: [PATCH 07/34] Prevent null character in Windows home directory (#808) (#813) --- Sources/FoundationEssentials/String/String+Path.swift | 4 ++-- Sources/FoundationEssentials/URL/URL.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 79bcbfa76..b51625ee5 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -374,7 +374,7 @@ extension String { guard GetEnvironmentVariableW(pwszVariable, $0.baseAddress, dwLength) == dwLength - 1 else { return nil } - return String(decoding: $0, as: UTF16.self) + return String(decodingCString: $0.baseAddress!, as: UTF16.self) } } } @@ -436,7 +436,7 @@ extension String { guard GetUserProfileDirectoryW(hToken, $0.baseAddress, &dwcchSize) else { fatalError("unable to query user profile directory") } - return String(decoding: $0, as: UTF16.self) + return String(decodingCString: $0.baseAddress!, as: UTF16.self) } #else #if targetEnvironment(simulator) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index 56a88db60..e83c01ab1 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -1324,7 +1324,7 @@ public struct URL: Equatable, Sendable, Hashable { if result.count > 1 && result.utf8.last == UInt8(ascii: "/") { _ = result.popLast() } - let charsToLeaveEncoded = Set([UInt8(ascii: "/")]) + let charsToLeaveEncoded: Set = [._slash, 0] return Parser.percentDecode(result, excluding: charsToLeaveEncoded) ?? "" } From 9c43b5725c6656c37647f5677760eaf510d2770c Mon Sep 17 00:00:00 2001 From: Charles Hu Date: Mon, 5 Aug 2024 16:09:18 -0700 Subject: [PATCH 08/34] [6.0] Upgrade SwiftFoundationICU to version 74 (#809) --- Package.swift | 2 +- .../LocaleTests.swift | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/Package.swift b/Package.swift index 70f77537b..daaf6389b 100644 --- a/Package.swift +++ b/Package.swift @@ -62,7 +62,7 @@ var dependencies: [Package.Dependency] { from: "1.1.0"), .package( url: "https://github.com/apple/swift-foundation-icu", - exact: "0.0.9"), + branch: "release/6.0"), .package( url: "https://github.com/swiftlang/swift-syntax", from: "600.0.0-latest") diff --git a/Tests/FoundationInternationalizationTests/LocaleTests.swift b/Tests/FoundationInternationalizationTests/LocaleTests.swift index 66304e224..c9dbdfa7f 100644 --- a/Tests/FoundationInternationalizationTests/LocaleTests.swift +++ b/Tests/FoundationInternationalizationTests/LocaleTests.swift @@ -161,15 +161,9 @@ final class LocaleTests : XCTestCase { return localeComponents } -#if FIXED_ICU_20187 verify(cldr: "root", bcp47: "und", icu: "") { return Locale.Components(identifier: "") } -#else - verify(cldr: "root", bcp47: "und", icu: "en_US_POSIX") { - return Locale.Components(identifier: "") - } -#endif verify(cldr: "und_US", bcp47: "und-US", icu: "_US") { return Locale.Components(languageRegion: .unitedStates) @@ -757,11 +751,7 @@ extension LocaleTests { // TODO: Reenable once (Locale.canonicalIdentifier) is implemented func test_identifierTypesFromSpecialIdentifier() throws { -#if FIXED_ICU_20187 verify("", cldr: "root", bcp47: "und", icu: "") -#else - verify("", cldr: "root", bcp47: "und", icu: "en_US_POSIX") -#endif verify("root", cldr: "root", bcp47: "root", icu: "root") verify("und", cldr: "root", bcp47: "und", icu: "und") @@ -780,20 +770,12 @@ extension LocaleTests { // If there's only one component, it is treated as the language code verify("123", cldr: "root", bcp47: "und", icu: "123") -#if FIXED_ICU_20187 verify("😀123", cldr: "root", bcp47: "und", icu: "") -#else - verify("😀123", cldr: "root", bcp47: "und", icu: "en_US_POSIX") -#endif // The "_" prefix marks the start of the region verify("_ZZ", cldr: "und_ZZ", bcp47: "und-ZZ", icu: "_ZZ") verify("_123", cldr: "und_123", bcp47: "und-123", icu: "_123") -#if FIXED_ICU_20187 verify("_😀123", cldr: "root", bcp47: "und", icu: "") -#else - verify("_😀123", cldr: "root", bcp47: "und", icu: "en_US_POSIX") -#endif // Starting an ID with script code is an acceptable special case verify("Hant", cldr: "hant", bcp47: "hant", icu: "hant") From 6a526e844ce84778b4b8f1031133d9a5f9eef51e Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld <1004103+jmschonfeld@users.noreply.github.com> Date: Tue, 6 Aug 2024 09:45:27 -0700 Subject: [PATCH 09/34] Remove _typeByName lookup of _FoundationNSNumberInitializer (#817) (#824) --- .../FoundationEssentials/FileManager/FileManager+Files.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index 72e05f937..b1cdba24a 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -120,8 +120,8 @@ public protocol _NSNumberInitializer { @_spi(SwiftCorelibsFoundation) dynamic public func _nsNumberInitializer() -> (any _NSNumberInitializer.Type)? { - // TODO: return nil here after swift-corelibs-foundation begins dynamically replacing this function - _typeByName("Foundation._FoundationNSNumberInitializer") as? any _NSNumberInitializer.Type + // Dynamically replaced by swift-corelibs-foundation + return nil } #endif From 3e81cb0de4fd90939c7bb9b19079c4ab70e72d1f Mon Sep 17 00:00:00 2001 From: kperryua Date: Tue, 6 Aug 2024 15:08:45 -0700 Subject: [PATCH 10/34] [6.0] rdar://132940984 (Regression: Swift Decoding of Double.greatestFiniteMagnitude as Int causes a crash) (#827) (#829) (cherry picked from commit c67f24ec7290fed0dd6ceafd2f840212de5776c5) --- Sources/FoundationEssentials/JSON/JSONDecoder.swift | 13 +++++++------ .../JSONEncoderTests.swift | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationEssentials/JSON/JSONDecoder.swift b/Sources/FoundationEssentials/JSON/JSONDecoder.swift index 0565e0f8d..fc9bf0b28 100644 --- a/Sources/FoundationEssentials/JSON/JSONDecoder.swift +++ b/Sources/FoundationEssentials/JSON/JSONDecoder.swift @@ -988,17 +988,18 @@ extension JSONDecoderImpl: Decoder { static private func _slowpath_unwrapFixedWidthInteger(as type: T.Type, json5: Bool, numberBuffer: BufferView, fullSource: BufferView, digitBeginning: BufferViewIndex, for codingPathNode: _CodingPathNode, _ additionalKey: (some CodingKey)?) throws -> T { // This is the slow path... If the fast path has failed. For example for "34.0" as an integer, we try to parse as either a Decimal or a Double and then convert back, losslessly. if let double = Double(prevalidatedBuffer: numberBuffer) { + // T.init(exactly:) guards against non-integer Double(s), but the parser may + // have already transformed the non-integer "1.0000000000000001" into 1, etc. + // Proper lossless behavior should be implemented by the parser. + guard let value = T(exactly: double) else { + throw JSONError.numberIsNotRepresentableInSwift(parsed: String(decoding: numberBuffer, as: UTF8.self)) + } + // The distance between Double(s) is >=2 from ±2^53. // 2^53 may represent either 2^53 or 2^53+1 rounded toward zero. // This code makes it so you don't get integer A from integer B. // Proper lossless behavior should be implemented by the parser. if double.magnitude < Double(sign: .plus, exponent: Double.significandBitCount + 1, significand: 1) { - // T.init(exactly:) guards against non-integer Double(s), but the parser may - // have already transformed the non-integer "1.0000000000000001" into 1, etc. - // Proper lossless behavior should be implemented by the parser. - guard let value = T(exactly: double) else { - throw JSONError.numberIsNotRepresentableInSwift(parsed: String(decoding: numberBuffer, as: UTF8.self)) - } return value } } diff --git a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift index ee192fd6e..20e29f345 100644 --- a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift +++ b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift @@ -1450,6 +1450,11 @@ final class JSONEncoderTests : XCTestCase { _testRoundTrip(of: testValue) } + func test_decodeLargeDoubleAsInteger() { + let data = try! JSONEncoder().encode(Double.greatestFiniteMagnitude) + XCTAssertThrowsError(try JSONDecoder().decode(UInt64.self, from: data)) + } + func test_localeDecimalPolicyIndependence() { var currentLocale: UnsafeMutablePointer? = nil if let localePtr = setlocale(LC_ALL, nil) { From 057297dad07367766ff4d4fd14412ee8301b4fc3 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld <1004103+jmschonfeld@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:23:39 -0700 Subject: [PATCH 11/34] Add FoundationNetworking hook for reading contents of remote URL (#820) (#826) --- Sources/FoundationEssentials/Data/Data.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/Data/Data.swift b/Sources/FoundationEssentials/Data/Data.swift index a88478261..8bded856a 100644 --- a/Sources/FoundationEssentials/Data/Data.swift +++ b/Sources/FoundationEssentials/Data/Data.swift @@ -2077,6 +2077,14 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect public static let fileProtectionMask = WritingOptions(rawValue: 0xf0000000) } #endif + + #if !FOUNDATION_FRAMEWORK + @_spi(SwiftCorelibsFoundation) + public dynamic init(_contentsOfRemote url: URL, options: ReadingOptions = []) throws { + assert(!url.isFileURL) + throw CocoaError(.fileReadUnsupportedScheme) + } + #endif /// Initialize a `Data` with the contents of a `URL`. /// @@ -2096,7 +2104,7 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect let d = try NSData(contentsOf: url, options: NSData.ReadingOptions(rawValue: options.rawValue)) self.init(referencing: d) #else - throw CocoaError(.fileReadUnsupportedScheme) + try self.init(_contentsOfRemote: url, options: options) #endif } #endif From 92b84e4c7516f98254fd1de62ec9151614f1fdeb Mon Sep 17 00:00:00 2001 From: Charles Hu Date: Fri, 9 Aug 2024 14:32:29 -0700 Subject: [PATCH 12/34] [6.0] Fix ProcessInfo.processName for Windows (#839) (#840) Instead of relying on hard coding PATH_SEPARATOR, use existing .lastPathComponent that already works on Windows --- Sources/FoundationEssentials/Platform.swift | 2 -- .../FoundationEssentials/ProcessInfo/ProcessInfo.swift | 8 +------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Sources/FoundationEssentials/Platform.swift b/Sources/FoundationEssentials/Platform.swift index 20dc561af..65b3449ef 100644 --- a/Sources/FoundationEssentials/Platform.swift +++ b/Sources/FoundationEssentials/Platform.swift @@ -49,8 +49,6 @@ package struct Platform { _pageSize } - // FIXME: Windows SEPARATOR - static let PATH_SEPARATOR: Character = "/" static let MAX_HOSTNAME_LENGTH = 1024 static func roundDownToMultipleOfPageSize(_ size: Int) -> Int { diff --git a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift index 302d3c62d..6f8b2b00a 100644 --- a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift +++ b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift @@ -581,13 +581,7 @@ extension _ProcessInfo { guard let processPath = CommandLine.arguments.first else { return "" } - - if let lastSlash = processPath.lastIndex(of: Platform.PATH_SEPARATOR) { - return String(processPath[ - processPath.index(after: lastSlash) ..< processPath.endIndex]) - } - - return processPath + return processPath.lastPathComponent } #if os(macOS) From 1b79d86584fe8e5928ef01a7a4830c0ed867e67f Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld <1004103+jmschonfeld@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:43:00 -0700 Subject: [PATCH 13/34] Enable WMO (#832) (#841) --- CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b99fcfa8f..fd8e463de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,16 @@ if(NOT SWIFT_SYSTEM_NAME) endif() endif() +# Don't enable WMO on Windows due to linker failures +if(NOT CMAKE_SYSTEM_NAME STREQUAL Windows) + # Enable whole module optimization for release builds & incremental for debug builds + if(POLICY CMP0157) + set(CMAKE_Swift_COMPILATION_MODE "$,wholemodule,incremental>") + else() + add_compile_options($<$,$>:-wmo>) + endif() +endif() + 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) From eb3e4bc55d486fc931c99868432279cb7aaca184 Mon Sep 17 00:00:00 2001 From: Saleem Abdulrasool Date: Thu, 15 Aug 2024 15:10:18 -0700 Subject: [PATCH 14/34] FoundationMacros: use cross-compilation to build for host (#714) (#849) Use `ExternalProject` to switch FoundationMacros to cross-compilation. This allows us to build the macros for the right OS/architecture when cross-compiling Foundation for other environments. Co-authored-by: Alex Lorenz --- CMakeLists.txt | 2 + Sources/CMakeLists.txt | 6 --- Sources/FoundationEssentials/CMakeLists.txt | 7 ++- Sources/FoundationMacros/CMakeLists.txt | 53 ++++++++++++++------- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fd8e463de..a8dc410cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,8 @@ set(BUILD_TESTING NO) set(COLLECTIONS_SINGLE_MODULE YES) set(COLLECTIONS_FOUNDATION_TOOLCHAIN_MODULE YES) +set(SwiftFoundation_MACRO "" CACHE STRING "Path to Foundation macro plugin") + # Make sure our dependencies exists include(FetchContent) if (_SwiftFoundationICU_SourceDIR) diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index f7f7ba156..ef235c43f 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -13,11 +13,5 @@ ##===----------------------------------------------------------------------===## add_subdirectory(_FoundationCShims) - -# Disable the macro build on Windows until we can correctly build it for the host architecture -if(NOT CMAKE_SYSTEM_NAME STREQUAL Windows) - add_subdirectory(FoundationMacros) -endif() - add_subdirectory(FoundationEssentials) add_subdirectory(FoundationInternationalization) diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index d9ebc2ebb..6dc5929d4 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -48,10 +48,9 @@ add_subdirectory(String) add_subdirectory(TimeZone) add_subdirectory(URL) -if(NOT CMAKE_SYSTEM_NAME STREQUAL Windows) - # Depend on FoundationMacros - add_dependencies(FoundationEssentials FoundationMacros) - target_compile_options(FoundationEssentials PRIVATE -plugin-path ${CMAKE_BINARY_DIR}/lib) +if(SwiftFoundation_MACRO) + target_compile_options(FoundationEssentials PRIVATE + "SHELL:-plugin-path ${SwiftFoundation_MACRO}") endif() if(CMAKE_SYSTEM_NAME STREQUAL Linux OR CMAKE_SYSTEM_NAME STREQUAL Android) diff --git a/Sources/FoundationMacros/CMakeLists.txt b/Sources/FoundationMacros/CMakeLists.txt index c95c5419e..97104767c 100644 --- a/Sources/FoundationMacros/CMakeLists.txt +++ b/Sources/FoundationMacros/CMakeLists.txt @@ -12,29 +12,45 @@ ## ##===----------------------------------------------------------------------===## +cmake_minimum_required(VERSION 3.22) + +if(POLICY CMP0156) + # Deduplicate linked libraries where appropriate + cmake_policy(SET CMP0156 NEW) +endif() +if(POLICY CMP0157) + # New Swift build model: improved incremental build performance and LSP support + cmake_policy(SET CMP0157 NEW) +endif() + +project(FoundationMacros + LANGUAGES Swift) + # SwiftSyntax Dependency -include(FetchContent) -find_package(SwiftSyntax) +find_package(SwiftSyntax QUIET) if(NOT SwiftSyntax_FOUND) + include(FetchContent) + # If building at desk, check out and link against the SwiftSyntax repo's targets FetchContent_Declare(SwiftSyntax GIT_REPOSITORY https://github.com/swiftlang/swift-syntax.git GIT_TAG 4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c) # 600.0.0-prerelease-2024-06-12 FetchContent_MakeAvailable(SwiftSyntax) +else() + message(STATUS "Using swift-syntax from ${SwiftSyntax_DIR}") endif() add_library(FoundationMacros SHARED FoundationMacros.swift PredicateMacro.swift) - + target_compile_definitions(FoundationMacros PRIVATE FOUNDATION_MACROS_LIBRARY) -set_target_properties(FoundationMacros - PROPERTIES - ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_BINARY_DIR}/lib -) +target_compile_options(FoundationMacros PRIVATE -parse-as-library) +target_compile_options(FoundationMacros PRIVATE + "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend AccessLevelOnImport>" + "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend StrictConcurrency>" + "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend InferSendableFromCaptures>") target_link_libraries(FoundationMacros PUBLIC SwiftSyntax::SwiftSyntax @@ -43,18 +59,19 @@ target_link_libraries(FoundationMacros PUBLIC SwiftSyntax::SwiftSyntaxBuilder ) -# The macro is installed into lib/swift/host/plugins, but needs to load libraries from lib/swift/host and lib/swift/${SWIFT_SYSTEM_NAME} set_target_properties(FoundationMacros PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib + PDB_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_BINARY_DIR}/lib + + # The macro is installed into lib/swift/host/plugins, but needs to load + # libraries from lib/swift/host and lib/swift/${SWIFT_SYSTEM_NAME} INSTALL_RPATH "$ORIGIN/../../../swift/${SWIFT_SYSTEM_NAME}:$ORIGIN/.." INSTALL_REMOVE_ENVIRONMENT_RPATH ON) -target_compile_options(FoundationMacros PRIVATE -parse-as-library) -target_compile_options(FoundationMacros PRIVATE - "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend AccessLevelOnImport>" - "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend StrictConcurrency>" - "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend InferSendableFromCaptures>") - install(TARGETS FoundationMacros - ARCHIVE DESTINATION lib/swift/host/plugins LIBRARY DESTINATION lib/swift/host/plugins - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + RUNTIME DESTINATION bin) From 07b7a5daa9d89278ae6f7263308edc1aaa683185 Mon Sep 17 00:00:00 2001 From: Evan Wilde Date: Fri, 16 Aug 2024 09:32:26 -0700 Subject: [PATCH 15/34] Get Swift-Foundation building for Musl/Static SDK (#838) * Get FoundationEssentials building Adding the missing musl imports to get FoundationEssentials building for the Swift static SDKs again. Also providing an option to disable building the macros. The macros aren't necessary for building the library and will not be run as part of the static SDK. No need to bloat the SDK or build times further. For Swift 6, the macros should be provided by the toolchain since the toolchain and SDK are current revlocked due to swiftmodules. * Get FoundationInternationalization building Adding the missing Musl imports to get FoundationInternationalization building for the static SDK. --- Sources/FoundationEssentials/Calendar/Calendar.swift | 2 ++ .../Calendar/Calendar_Gregorian.swift | 2 ++ Sources/FoundationEssentials/Data/Data+Reading.swift | 4 +++- Sources/FoundationEssentials/Data/Data+Writing.swift | 4 +++- Sources/FoundationEssentials/Date.swift | 2 ++ .../FoundationEssentials/Decimal/Decimal+Math.swift | 2 ++ .../Error/CocoaError+FilePath.swift | 2 ++ .../FoundationEssentials/Error/ErrorCodes+POSIX.swift | 2 ++ .../FileManager/FileManager+Basics.swift | 2 ++ .../FileManager/FileManager+Directories.swift | 2 ++ .../FileManager/FileManager+Files.swift | 3 +++ .../FileManager/FileManager+SymbolicLinks.swift | 2 ++ .../FileManager/FileManager+Utilities.swift | 3 +++ .../FileManager/FileOperations+Enumeration.swift | 5 ++++- .../FileManager/FileOperations.swift | 2 ++ .../BinaryInteger+NumericStringRepresentation.swift | 2 ++ Sources/FoundationEssentials/LockedState.swift | 4 +++- Sources/FoundationEssentials/Platform.swift | 3 +++ .../FoundationEssentials/ProcessInfo/ProcessInfo.swift | 6 ++++-- .../PropertyList/OpenStepPlist.swift | 2 ++ Sources/FoundationEssentials/String/String+Path.swift | 2 ++ .../FoundationEssentials/TimeZone/TimeZone_Cache.swift | 2 ++ Sources/FoundationEssentials/_ThreadLocal.swift | 10 ++++++---- .../Calendar/Calendar_ICU.swift | 2 ++ Sources/FoundationInternationalization/Date+ICU.swift | 2 ++ .../Formatting/Date/ICUDateFormatter.swift | 2 ++ .../Formatting/Duration+Formatting.swift | 2 ++ 27 files changed, 68 insertions(+), 10 deletions(-) diff --git a/Sources/FoundationEssentials/Calendar/Calendar.swift b/Sources/FoundationEssentials/Calendar/Calendar.swift index 99d72e161..9de55b372 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar.swift @@ -16,6 +16,8 @@ internal import os import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(CRT) import CRT #endif diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift index fd863cef8..797a8e8b7 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift @@ -16,6 +16,8 @@ internal import os import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(CRT) import CRT #endif diff --git a/Sources/FoundationEssentials/Data/Data+Reading.swift b/Sources/FoundationEssentials/Data/Data+Reading.swift index eb66e3f7c..2540b14eb 100644 --- a/Sources/FoundationEssentials/Data/Data+Reading.swift +++ b/Sources/FoundationEssentials/Data/Data+Reading.swift @@ -22,6 +22,8 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK @@ -30,7 +32,7 @@ import WinSDK func _fgetxattr(_ fd: Int32, _ name: UnsafePointer!, _ value: UnsafeMutableRawPointer!, _ size: Int, _ position: UInt32, _ options: Int32) -> Int { #if canImport(Darwin) return fgetxattr(fd, name, value, size, position, options) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) return fgetxattr(fd, name, value, size) #else return -1 diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 4f92cb1a6..1e75b43cf 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -24,6 +24,8 @@ import Android import unistd #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK @@ -623,7 +625,7 @@ private func writeExtendedAttributes(fd: Int32, attributes: [String : Data]) { // Returns non-zero on error, but we ignore them #if canImport(Darwin) _ = fsetxattr(fd, key, valueBuf.baseAddress!, valueBuf.count, 0, 0) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) _ = fsetxattr(fd, key, valueBuf.baseAddress!, valueBuf.count, 0) #endif } diff --git a/Sources/FoundationEssentials/Date.swift b/Sources/FoundationEssentials/Date.swift index 8811aa433..b65066f14 100644 --- a/Sources/FoundationEssentials/Date.swift +++ b/Sources/FoundationEssentials/Date.swift @@ -16,6 +16,8 @@ import Darwin import Bionic #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(WinSDK) import WinSDK #endif diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift index 172454463..c06be7abd 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift @@ -16,6 +16,8 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(CRT) import CRT #endif diff --git a/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift b/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift index fbce0f6f8..d9b249761 100644 --- a/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift +++ b/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift @@ -19,6 +19,8 @@ import Darwin import Bionic #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK diff --git a/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift b/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift index a7e01952c..048cd29b2 100644 --- a/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift +++ b/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift @@ -14,6 +14,8 @@ @preconcurrency import Android #elseif canImport(Glibc) @preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl #elseif canImport(Darwin) @preconcurrency import Darwin #elseif os(Windows) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift b/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift index 03ca025a2..b49821b1a 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift @@ -16,6 +16,8 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift index 89e3f1f97..0941e5186 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift @@ -23,6 +23,8 @@ import Android import unistd #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index b1cdba24a..fdd601020 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -23,6 +23,9 @@ import posix_filesystem #elseif canImport(Glibc) import Glibc internal import _FoundationCShims +#elseif canImport(Musl) +import Musl +internal import _FoundationCShims #elseif os(Windows) import CRT import WinSDK diff --git a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift index fc9b8f70e..a1355e78d 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift @@ -17,6 +17,8 @@ import Android import unistd #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift index bba8ed5c9..9bac9676f 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift @@ -28,6 +28,9 @@ import Android #elseif canImport(Glibc) import Glibc internal import _FoundationCShims +#elseif canImport(Musl) +import Musl +internal import _FoundationCShims #elseif os(Windows) import CRT import WinSDK diff --git a/Sources/FoundationEssentials/FileManager/FileOperations+Enumeration.swift b/Sources/FoundationEssentials/FileManager/FileOperations+Enumeration.swift index 22111f52c..2c9a02f6c 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations+Enumeration.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations+Enumeration.swift @@ -115,6 +115,9 @@ import posix_filesystem.dirent #elseif canImport(Glibc) import Glibc internal import _FoundationCShims +#elseif canImport(Musl) +import Musl +internal import _FoundationCShims #endif // MARK: Directory Iteration @@ -318,7 +321,7 @@ extension Sequence<_FTSSequence.Element> { struct _POSIXDirectoryContentsSequence: Sequence { #if canImport(Darwin) typealias DirectoryEntryPtr = UnsafeMutablePointer - #elseif os(Android) || canImport(Glibc) + #elseif os(Android) || canImport(Glibc) || canImport(Musl) typealias DirectoryEntryPtr = OpaquePointer #endif diff --git a/Sources/FoundationEssentials/FileManager/FileOperations.swift b/Sources/FoundationEssentials/FileManager/FileOperations.swift index 8e67a771f..f7f77b44d 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations.swift @@ -16,6 +16,8 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK diff --git a/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift b/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift index db02789e2..43e9fcdaa 100644 --- a/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift +++ b/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift @@ -16,6 +16,8 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT #endif diff --git a/Sources/FoundationEssentials/LockedState.swift b/Sources/FoundationEssentials/LockedState.swift index ad432c704..6eb9ad840 100644 --- a/Sources/FoundationEssentials/LockedState.swift +++ b/Sources/FoundationEssentials/LockedState.swift @@ -19,6 +19,8 @@ internal import C.os.lock import Bionic #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(WinSDK) import WinSDK #endif @@ -29,7 +31,7 @@ package struct LockedState { private struct _Lock { #if canImport(os) typealias Primitive = os_unfair_lock -#elseif os(Android) || canImport(Glibc) +#elseif os(Android) || canImport(Glibc) || canImport(Musl) typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK diff --git a/Sources/FoundationEssentials/Platform.swift b/Sources/FoundationEssentials/Platform.swift index 65b3449ef..9c3f2d7a3 100644 --- a/Sources/FoundationEssentials/Platform.swift +++ b/Sources/FoundationEssentials/Platform.swift @@ -35,6 +35,9 @@ fileprivate let _pageSize: Int = Int(getpagesize()) #elseif canImport(Glibc) import Glibc fileprivate let _pageSize: Int = Int(getpagesize()) +#elseif canImport(Musl) +import Musl +fileprivate let _pageSize: Int = Int(getpagesize()) #elseif canImport(C) fileprivate let _pageSize: Int = Int(getpagesize()) #endif // canImport(Darwin) diff --git a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift index 6f8b2b00a..2e809fa70 100644 --- a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift +++ b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift @@ -19,6 +19,8 @@ import Bionic import unistd #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import WinSDK #endif @@ -161,7 +163,7 @@ final class _ProcessInfo: Sendable { } var userName: String { -#if canImport(Darwin) || os(Android) || canImport(Glibc) +#if canImport(Darwin) || os(Android) || canImport(Glibc) || canImport(Musl) // Darwin and Linux let (euid, _) = Platform.getUGIDs() if let upwd = getpwuid(euid), @@ -196,7 +198,7 @@ final class _ProcessInfo: Sendable { } var fullUserName: String { -#if canImport(Darwin) || os(Android) || canImport(Glibc) +#if canImport(Darwin) || os(Android) || canImport(Glibc) || canImport(Musl) let (euid, _) = Platform.getUGIDs() if let upwd = getpwuid(euid), let fullname = upwd.pointee.pw_gecos { diff --git a/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift b/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift index 174a6edda..c0428202d 100644 --- a/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift +++ b/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift @@ -16,6 +16,8 @@ import Darwin import Bionic #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #endif #if canImport(CRT) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index b51625ee5..37cc43f25 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -16,6 +16,8 @@ internal import os import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import WinSDK #endif diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift index 1243def34..744d77b3b 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift @@ -16,6 +16,8 @@ import Darwin import unistd #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(ucrt) import ucrt #endif diff --git a/Sources/FoundationEssentials/_ThreadLocal.swift b/Sources/FoundationEssentials/_ThreadLocal.swift index 67f5dfaae..aea9c4116 100644 --- a/Sources/FoundationEssentials/_ThreadLocal.swift +++ b/Sources/FoundationEssentials/_ThreadLocal.swift @@ -15,6 +15,8 @@ import Darwin import Bionic #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(WinSDK) import WinSDK #elseif canImport(threads_h) @@ -24,7 +26,7 @@ internal import threads #endif struct _ThreadLocal { -#if canImport(Darwin) || os(Android) || canImport(Glibc) +#if canImport(Darwin) || os(Android) || canImport(Glibc) || canImport(Musl) fileprivate typealias PlatformKey = pthread_key_t #elseif USE_TSS fileprivate typealias PlatformKey = tss_t @@ -36,7 +38,7 @@ struct _ThreadLocal { fileprivate let key: PlatformKey init() { -#if canImport(Darwin) || os(Android) || canImport(Glibc) +#if canImport(Darwin) || os(Android) || canImport(Glibc) || canImport(Musl) var key = PlatformKey() pthread_key_create(&key, nil) self.key = key @@ -52,7 +54,7 @@ struct _ThreadLocal { private static subscript(_ key: PlatformKey) -> UnsafeMutableRawPointer? { get { -#if canImport(Darwin) || os(Android) || canImport(Glibc) +#if canImport(Darwin) || os(Android) || canImport(Glibc) || canImport(Musl) pthread_getspecific(key) #elseif USE_TSS tss_get(key) @@ -62,7 +64,7 @@ struct _ThreadLocal { } set { -#if canImport(Darwin) || os(Android) || canImport(Glibc) +#if canImport(Darwin) || os(Android) || canImport(Glibc) || canImport(Musl) pthread_setspecific(key, newValue) #elseif USE_TSS tss_set(key, newValue) diff --git a/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift b/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift index 0f0b97386..0d3c3710a 100644 --- a/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift +++ b/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift @@ -18,6 +18,8 @@ import FoundationEssentials import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(CRT) import CRT #elseif canImport(Darwin) diff --git a/Sources/FoundationInternationalization/Date+ICU.swift b/Sources/FoundationInternationalization/Date+ICU.swift index 3895915ea..b91cd98b6 100644 --- a/Sources/FoundationInternationalization/Date+ICU.swift +++ b/Sources/FoundationInternationalization/Date+ICU.swift @@ -19,6 +19,8 @@ internal import _FoundationICU import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(Darwin) import Darwin #endif diff --git a/Sources/FoundationInternationalization/Formatting/Date/ICUDateFormatter.swift b/Sources/FoundationInternationalization/Formatting/Date/ICUDateFormatter.swift index d6ce6cf0a..1cd3bde72 100644 --- a/Sources/FoundationInternationalization/Formatting/Date/ICUDateFormatter.swift +++ b/Sources/FoundationInternationalization/Formatting/Date/ICUDateFormatter.swift @@ -22,6 +22,8 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #endif typealias UChar = UInt16 diff --git a/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift b/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift index ccfea7873..a94f57161 100644 --- a/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift +++ b/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift @@ -20,6 +20,8 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT #endif From d8d45111c0ad969b0b5cbdf5d0478dc85b8e1b26 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld <1004103+jmschonfeld@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:09:49 -0700 Subject: [PATCH 16/34] Use fallback home directory on Windows (#854) (#862) * Use %SystemDrive\Users\Public as fallback home directory on Windows * Update to ALLUSERSPROFILE * Fix test failure * Fix iOS test failure --- .../String/String+Path.swift | 26 +++++++------------ .../FileManager/FileManagerTests.swift | 15 +++++++++++ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 37cc43f25..799e552d7 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -367,33 +367,27 @@ extension String { #if !NO_FILESYSTEM internal static func homeDirectoryPath(forUser user: String? = nil) -> String { #if os(Windows) - func GetUserProfile() -> String? { - return "USERPROFILE".withCString(encodedAs: UTF16.self) { pwszVariable in - let dwLength: DWORD = GetEnvironmentVariableW(pwszVariable, nil, 0) - // Ensure that `USERPROFILE` is defined. - if dwLength == 0 { return nil } - return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { - guard GetEnvironmentVariableW(pwszVariable, $0.baseAddress, dwLength) == dwLength - 1 else { - return nil - } - return String(decodingCString: $0.baseAddress!, as: UTF16.self) + if let user { + func fallbackUserDirectory() -> String { + guard let fallback = ProcessInfo.processInfo.environment["ALLUSERSPROFILE"] else { + fatalError("Unable to find home directory for user \(user) and ALLUSERSPROFILE environment variable is not set") } + + return fallback } - } - - if let user { + return user.withCString(encodedAs: UTF16.self) { pwszUserName in var cbSID: DWORD = 0 var cchReferencedDomainName: DWORD = 0 var eUse: SID_NAME_USE = SidTypeUnknown guard LookupAccountNameW(nil, pwszUserName, nil, &cbSID, nil, &cchReferencedDomainName, &eUse) else { - fatalError("unable to lookup SID for user \(user)") + return fallbackUserDirectory() } return withUnsafeTemporaryAllocation(of: CChar.self, capacity: Int(cbSID)) { pSID in return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(cchReferencedDomainName)) { pwszReferencedDomainName in guard LookupAccountNameW(nil, pwszUserName, pSID.baseAddress, &cbSID, pwszReferencedDomainName.baseAddress, &cchReferencedDomainName, &eUse) else { - fatalError("unable to lookup SID for user \(user)") + return fallbackUserDirectory() } var pwszSID: LPWSTR? = nil @@ -423,7 +417,7 @@ extension String { var hToken: HANDLE? = nil guard OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken) else { - guard let UserProfile = GetUserProfile() else { + guard let UserProfile = ProcessInfo.processInfo.environment["UserProfile"] else { fatalError("unable to evaluate `%UserProfile%`") } return UserProfile diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 0cae49739..0e2d2eec8 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -897,4 +897,19 @@ final class FileManagerTests : XCTestCase { } } } + + func testHomeDirectoryForNonExistantUser() throws { + #if canImport(Darwin) && !os(macOS) + throw XCTSkip("This test is not applicable on this platform") + #else + #if os(Windows) + let fallbackPath = URL(filePath: try XCTUnwrap(ProcessInfo.processInfo.environment["ALLUSERSPROFILE"]), directoryHint: .isDirectory) + #else + let fallbackPath = URL(filePath: "/var/empty", directoryHint: .isDirectory) + #endif + + XCTAssertEqual(FileManager.default.homeDirectory(forUser: ""), fallbackPath) + XCTAssertEqual(FileManager.default.homeDirectory(forUser: UUID().uuidString), fallbackPath) + #endif + } } From 5b61e03f4e9a0931432e918a8e384e4b13c8f163 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Fri, 16 Aug 2024 11:14:38 -0700 Subject: [PATCH 17/34] Fix Windows symlink handling in FileManager APIs (#858) (#865) * Fix Windows symlink handling in FileManager APIs * Address feedback --- .../FileManager/FileManager+Basics.swift | 23 ++++++--- .../FileManager/FileOperations.swift | 15 ++++-- .../FileManager/FileManagerPlayground.swift | 22 +++++++++ .../FileManager/FileManagerTests.swift | 47 ++++++++++++++++++- 4 files changed, 97 insertions(+), 10 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift b/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift index b49821b1a..991c5e817 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift @@ -85,14 +85,14 @@ internal struct _FileManagerImpl { ) -> Bool { #if os(Windows) return (try? path.withNTPathRepresentation { - let hLHS = CreateFileW($0, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nil) + let hLHS = CreateFileW($0, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nil) if hLHS == INVALID_HANDLE_VALUE { return false } defer { CloseHandle(hLHS) } return (try? other.withNTPathRepresentation { - let hRHS = CreateFileW($0, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nil) + let hRHS = CreateFileW($0, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nil) if hRHS == INVALID_HANDLE_VALUE { return false } @@ -129,11 +129,21 @@ internal struct _FileManagerImpl { return false } - if fbiLHS.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT, - fbiRHS.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT { + let lhsIsReparsePoint = fbiLHS.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT + let rhsIsReparsePoint = fbiRHS.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT + let lhsIsDirectory = fbiLHS.FileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY + let rhsIsDirectory = fbiRHS.FileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY + + guard lhsIsReparsePoint == rhsIsReparsePoint, lhsIsDirectory == rhsIsDirectory else { + // If they aren't the same "type", then they cannot be equivalent + return false + } + + if lhsIsReparsePoint { + // Both are symbolic links, so they are equivalent if their destinations are equivalent return (try? fileManager.destinationOfSymbolicLink(atPath: path) == fileManager.destinationOfSymbolicLink(atPath: other)) ?? false - } else if fbiLHS.FileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY, - fbiRHS.FileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY { + } else if lhsIsDirectory { + // Both are directories, so recursively compare the directories guard let aLHSItems = try? fileManager.contentsOfDirectory(atPath: path), let aRHSItems = try? fileManager.contentsOfDirectory(atPath: other), aLHSItems == aRHSItems else { @@ -160,6 +170,7 @@ internal struct _FileManagerImpl { return true } else { + // Both must be standard files, so binary compare the contents of the files var liLHSSize: LARGE_INTEGER = .init() var liRHSSize: LARGE_INTEGER = .init() guard GetFileSizeEx(hLHS, &liLHSSize), GetFileSizeEx(hRHS, &liRHSSize), LARGE_INTEGER._equals(liLHSSize, liRHSSize) else { diff --git a/Sources/FoundationEssentials/FileManager/FileOperations.swift b/Sources/FoundationEssentials/FileManager/FileOperations.swift index f7f77b44d..03adcc6fa 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations.swift @@ -807,7 +807,16 @@ enum _FileOperations { guard delegate.shouldPerformOnItemAtPath(src, to: dst) else { return } try dst.withNTPathRepresentation { pwszDestination in - if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY { + // Check for reparse points first because symlinks to directories are reported as both reparse points and directories, and we should copy the symlink not the contents of the linked directory + if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT { + do { + let linkDest = try fileManager.destinationOfSymbolicLink(atPath: src) + try fileManager.createSymbolicLink(atPath: dst, withDestinationPath: linkDest) + } catch { + try delegate.throwIfNecessary(error, src, dst) + return + } + } else if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY { do { try fileManager.createDirectory(atPath: dst, withIntermediateDirectories: true) } catch { @@ -816,10 +825,10 @@ enum _FileOperations { for item in _Win32DirectoryContentsSequence(path: src, appendSlashForDirectory: true) { try linkOrCopyFile(src.appendingPathComponent(item.fileName), dst: dst.appendingPathComponent(item.fileName), with: fileManager, delegate: delegate) } - } else if bCopyFile || faAttributes.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT { + } else if bCopyFile { var ExtendedParameters: COPYFILE2_EXTENDED_PARAMETERS = .init() ExtendedParameters.dwSize = DWORD(MemoryLayout.size) - ExtendedParameters.dwCopyFlags = COPY_FILE_FAIL_IF_EXISTS | COPY_FILE_COPY_SYMLINK | COPY_FILE_NO_BUFFERING | COPY_FILE_OPEN_AND_COPY_REPARSE_POINT + ExtendedParameters.dwCopyFlags = COPY_FILE_FAIL_IF_EXISTS | COPY_FILE_NO_BUFFERING if FAILED(CopyFile2(pwszSource, pwszDestination, &ExtendedParameters)) { try delegate.throwIfNecessary(GetLastError(), src, dst) diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerPlayground.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerPlayground.swift index af6b56f08..26a96919f 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerPlayground.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerPlayground.swift @@ -46,6 +46,22 @@ struct File : ExpressibleByStringLiteral, Buildable { } } +struct SymbolicLink : Buildable { + fileprivate let name: String + private let destination: String + + init(_ name: String, destination: String) { + self.name = name + self.destination = destination + } + + fileprivate func build(in path: String, using fileManager: FileManager) throws { + let linkPath = path.appendingPathComponent(name) + let destPath = path.appendingPathComponent(destination) + try fileManager.createSymbolicLink(atPath: linkPath, withDestinationPath: destPath) + } +} + struct Directory : Buildable { fileprivate let name: String private let attributes: [FileAttributeKey : Any]? @@ -70,11 +86,13 @@ struct FileManagerPlayground { enum Item : Buildable { case file(File) case directory(Directory) + case symbolicLink(SymbolicLink) fileprivate func build(in path: String, using fileManager: FileManager) throws { switch self { case let .file(file): try file.build(in: path, using: fileManager) case let .directory(dir): try dir.build(in: path, using: fileManager) + case let .symbolicLink(symlink): try symlink.build(in: path, using: fileManager) } } } @@ -92,6 +110,10 @@ struct FileManagerPlayground { static func buildExpression(_ expression: Directory) -> Item { .directory(expression) } + + static func buildExpression(_ expression: SymbolicLink) -> Item { + .symbolicLink(expression) + } } private let directory: Directory diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 0e2d2eec8..96d911a61 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -214,10 +214,20 @@ final class FileManagerTests : XCTestCase { File("Baz", contents: randomData()) } } + Directory("symlinks") { + File("Foo", contents: randomData()) + SymbolicLink("LinkToFoo", destination: "Foo") + } + Directory("EmptyDirectory") {} + "EmptyFile" }.test { XCTAssertTrue($0.contentsEqual(atPath: "dir1", andPath: "dir1_copy")) XCTAssertFalse($0.contentsEqual(atPath: "dir1/dir2", andPath: "dir1/dir3")) XCTAssertFalse($0.contentsEqual(atPath: "dir1", andPath: "dir1_diffdata")) + XCTAssertFalse($0.contentsEqual(atPath: "symlinks/LinkToFoo", andPath: "symlinks/Foo"), "Symbolic link should not be equal to its destination") + XCTAssertFalse($0.contentsEqual(atPath: "symlinks/LinkToFoo", andPath: "EmptyFile"), "Symbolic link should not be equal to an empty file") + XCTAssertFalse($0.contentsEqual(atPath: "symlinks/LinkToFoo", andPath: "EmptyDirectory"), "Symbolic link should not be equal to an empty directory") + XCTAssertFalse($0.contentsEqual(atPath: "symlinks/EmptyDirectory", andPath: "EmptyFile"), "Empty directory should not be equal to empty file") } } @@ -253,21 +263,30 @@ final class FileManagerTests : XCTestCase { "Baz" } } + Directory("symlinks") { + "Foo" + SymbolicLink("Bar", destination: "Foo") + SymbolicLink("Parent", destination: "..") + } }.test { XCTAssertEqual(try $0.subpathsOfDirectory(atPath: "dir1").sorted(), ["dir2", "dir2/Bar", "dir2/Foo", "dir3", "dir3/Baz"]) XCTAssertEqual(try $0.subpathsOfDirectory(atPath: "dir1/dir2").sorted(), ["Bar", "Foo"]) XCTAssertEqual(try $0.subpathsOfDirectory(atPath: "dir1/dir3").sorted(), ["Baz"]) + + XCTAssertEqual(try $0.subpathsOfDirectory(atPath: "symlinks").sorted(), ["Bar", "Foo", "Parent"]) + XCTAssertThrowsError(try $0.subpathsOfDirectory(atPath: "does_not_exist")) { XCTAssertEqual(($0 as? CocoaError)?.code, .fileReadNoSuchFile) } - let fullContents = ["dir1", "dir1/dir2", "dir1/dir2/Bar", "dir1/dir2/Foo", "dir1/dir3", "dir1/dir3/Baz"] + let fullContents = ["dir1", "dir1/dir2", "dir1/dir2/Bar", "dir1/dir2/Foo", "dir1/dir3", "dir1/dir3/Baz", "symlinks", "symlinks/Bar", "symlinks/Foo", "symlinks/Parent"] let cwd = $0.currentDirectoryPath XCTAssertNotEqual(cwd.last, "/") let paths = [cwd, "\(cwd)/", "\(cwd)//", ".", "./", ".//"] for path in paths { XCTAssertEqual(try $0.subpathsOfDirectory(atPath: path).sorted(), fullContents) } + } } @@ -345,6 +364,32 @@ final class FileManagerTests : XCTestCase { XCTAssertEqual($0.delegateCaptures.shouldCopy, [.init("foo", "bar")]) XCTAssertEqual($0.delegateCaptures.shouldProceedAfterCopyError, [.init("foo", "bar", code: .fileWriteFileExists)]) } + + try FileManagerPlayground { + "foo" + SymbolicLink("bar", destination: "foo") + }.test(captureDelegateCalls: true) { + XCTAssertTrue($0.delegateCaptures.isEmpty) + try $0.copyItem(atPath: "bar", toPath: "copy") + XCTAssertEqual($0.delegateCaptures.shouldCopy, [.init("bar", "copy")]) + XCTAssertEqual($0.delegateCaptures.shouldProceedAfterCopyError, []) + let copyDestination = try $0.destinationOfSymbolicLink(atPath: "copy") + XCTAssertEqual(copyDestination.lastPathComponent, "foo", "Copied symbolic link points at \(copyDestination) instead of foo") + } + + try FileManagerPlayground { + Directory("dir") { + "foo" + } + SymbolicLink("link", destination: "dir") + }.test(captureDelegateCalls: true) { + XCTAssertTrue($0.delegateCaptures.isEmpty) + try $0.copyItem(atPath: "link", toPath: "copy") + XCTAssertEqual($0.delegateCaptures.shouldCopy, [.init("link", "copy")]) + XCTAssertEqual($0.delegateCaptures.shouldProceedAfterCopyError, []) + let copyDestination = try $0.destinationOfSymbolicLink(atPath: "copy") + XCTAssertEqual(copyDestination.lastPathComponent, "dir", "Copied symbolic link points at \(copyDestination) instead of foo") + } } func testCreateSymbolicLinkAtPath() throws { From 0560549d2669f89de46d1563ec2c7f6ddfcdec1b Mon Sep 17 00:00:00 2001 From: Charles Hu Date: Fri, 16 Aug 2024 21:37:08 -0700 Subject: [PATCH 18/34] [6.0] Resolved an arithmetic overflow error in Decimal division caused by improper upcasting (#850) (#864) --- .../Decimal/Decimal+Math.swift | 2 +- .../DecimalTests.swift | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift index c06be7abd..7b35f1189 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift @@ -947,7 +947,7 @@ extension Decimal { // D1: Normalize // Calculate d such that `d*highest_dight_of_divisor >= b/2 (0x8000) - let d = (1 << 16) / UInt32(divisor[divisor.count - 1] + 1) + let d: UInt32 = (1 << 16) / (UInt32(divisor[divisor.count - 1]) + 1) // This is to make the whole algorithm work and // (dividend * d) / (divisor * d) == dividend / divisor var normalizedDividend = try self._integerMultiplyByShort( diff --git a/Tests/FoundationEssentialsTests/DecimalTests.swift b/Tests/FoundationEssentialsTests/DecimalTests.swift index b6c958f7d..9a1db4d56 100644 --- a/Tests/FoundationEssentialsTests/DecimalTests.swift +++ b/Tests/FoundationEssentialsTests/DecimalTests.swift @@ -613,6 +613,32 @@ final class DecimalTests : XCTestCase { XCTAssertTrue(Decimal._compare(lhs: expected, rhs: result) == .orderedSame) } + func testCrashingDivision() throws { + // This test makes sure the following division + // does not crash + let first: Decimal = Decimal(1147858867) + let second: Decimal = Decimal(4294967295) + let result = first / second + let expected: Decimal = Decimal( + _exponent: -38, + _length: 8, + _isNegative: 0, + _isCompact: 1, + _reserved: 0, + _mantissa: ( + 58076, + 13229, + 12316, + 25502, + 15252, + 32996, + 11611, + 5147 + ) + ) + XCTAssertEqual(result, expected) + } + func testPower() throws { var a = Decimal(1234) var result = try a._power(exponent: 0, roundingMode: .plain) @@ -1272,5 +1298,4 @@ final class DecimalTests : XCTestCase { XCTAssertEqual(length, 3) } #endif - } From 318ea46ccb70a935a8a92ac7d01cdc7466ad2ea6 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Mon, 19 Aug 2024 15:09:48 -0700 Subject: [PATCH 19/34] FileManager.fileExists(atPath:) should follow symlinks (#859) (#866) --- .../FileManager/FileManager+Files.swift | 15 +++++++++++---- .../FileManager/FileManagerTests.swift | 8 ++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index fdd601020..b8cd50a4c 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -376,12 +376,19 @@ extension _FileManagerImpl { private func _fileExists(_ path: String) -> (exists: Bool, isDirectory: Bool) { #if os(Windows) guard !path.isEmpty else { return (false, false) } - return (try? path.withNTPathRepresentation { - var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init() - guard GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) else { + return (try? path.withNTPathRepresentation { pwszPath in + let handle = CreateFileW(pwszPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nil) + if handle == INVALID_HANDLE_VALUE { + return (false, false) + } + defer { CloseHandle(handle) } + + var info: BY_HANDLE_FILE_INFORMATION = BY_HANDLE_FILE_INFORMATION() + guard GetFileInformationByHandle(handle, &info) else { return (false, false) } - return (true, faAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY) + + return (true, info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY) }) ?? (false, false) #else path.withFileSystemRepresentation { rep -> (Bool, Bool) in diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 96d911a61..42247bf82 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -571,6 +571,9 @@ final class FileManagerTests : XCTestCase { "bar" } "other" + SymbolicLink("link_to_file", destination: "other") + SymbolicLink("link_to_dir", destination: "dir") + SymbolicLink("link_to_nonexistent", destination: "does_not_exist") }.test { #if FOUNDATION_FRAMEWORK var isDir: ObjCBool = false @@ -591,7 +594,12 @@ final class FileManagerTests : XCTestCase { XCTAssertTrue(isDirBool()) XCTAssertTrue($0.fileExists(atPath: "other", isDirectory: &isDir)) XCTAssertFalse(isDirBool()) + XCTAssertTrue($0.fileExists(atPath: "link_to_file", isDirectory: &isDir)) + XCTAssertFalse(isDirBool()) + XCTAssertTrue($0.fileExists(atPath: "link_to_dir", isDirectory: &isDir)) + XCTAssertTrue(isDirBool()) XCTAssertFalse($0.fileExists(atPath: "does_not_exist")) + XCTAssertFalse($0.fileExists(atPath: "link_to_nonexistent")) } } From 56c44b0cc5c74c566f8e92f9ed0098278f2c266d Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Mon, 19 Aug 2024 15:11:15 -0700 Subject: [PATCH 20/34] Fix Windows home directory for specific user (#861) (#868) * Fix Windows home directory for specific user * Fix test failure --- .../FoundationEssentials/String/String+Path.swift | 12 +++++++++--- .../FileManager/FileManagerTests.swift | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 799e552d7..477d5d39b 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -375,12 +375,17 @@ extension String { return fallback } + + guard !user.isEmpty else { + return fallbackUserDirectory() + } return user.withCString(encodedAs: UTF16.self) { pwszUserName in var cbSID: DWORD = 0 var cchReferencedDomainName: DWORD = 0 var eUse: SID_NAME_USE = SidTypeUnknown - guard LookupAccountNameW(nil, pwszUserName, nil, &cbSID, nil, &cchReferencedDomainName, &eUse) else { + LookupAccountNameW(nil, pwszUserName, nil, &cbSID, nil, &cchReferencedDomainName, &eUse) + guard cbSID > 0 else { return fallbackUserDirectory() } @@ -395,10 +400,11 @@ extension String { fatalError("unable to convert SID to string for user \(user)") } - return #"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\#\(String(decodingCString: pwszSID!, as: UTF16.self))"#.withCString(encodedAs: UTF16.self) { pwszKeyPath in + return #"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\\#(String(decodingCString: pwszSID!, as: UTF16.self))"#.withCString(encodedAs: UTF16.self) { pwszKeyPath in return "ProfileImagePath".withCString(encodedAs: UTF16.self) { pwszKey in var cbData: DWORD = 0 - guard RegGetValueW(HKEY_LOCAL_MACHINE, pwszKeyPath, pwszKey, RRF_RT_REG_SZ, nil, nil, &cbData) == ERROR_SUCCESS else { + RegGetValueW(HKEY_LOCAL_MACHINE, pwszKeyPath, pwszKey, RRF_RT_REG_SZ, nil, nil, &cbData) + guard cbData > 0 else { fatalError("unable to query ProfileImagePath for user \(user)") } return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(cbData)) { pwszData in diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 42247bf82..a25638ab4 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -917,6 +917,15 @@ final class FileManagerTests : XCTestCase { try $0.setAttributes(attrs, ofItemAtPath: "foo") } } + + func testCurrentUserHomeDirectory() throws { + #if canImport(Darwin) && !os(macOS) + throw XCTSkip("This test is not applicable on this platform") + #else + let userName = ProcessInfo.processInfo.userName + XCTAssertEqual(FileManager.default.homeDirectory(forUser: userName), FileManager.default.homeDirectoryForCurrentUser) + #endif + } func testAttributesOfItemAtPath() throws { try FileManagerPlayground { From 720a0934a4b3be93d6440a3d27bbac4ca58b5e35 Mon Sep 17 00:00:00 2001 From: Pavel Yaskevich Date: Wed, 28 Aug 2024 09:57:15 -0700 Subject: [PATCH 21/34] Fix hard-coded path to `FoundationEssentialsTests` resources (#887) SwiftPM used to incorrectly build tests for both host and target. That has been fixed by https://github.com/swiftlang/swift-package-manager/pull/7879. The fix makes sure that if there are any direct macro dependencies in test targets xctest bundle is only built for "host". This means that the hard-coded resources path needs to be updated to include "-tool" suffix. (cherry picked from commit d14ceaf88961afce40fd34bd51c3296fbdcbb012) --- .../ResourceUtilities.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Tests/FoundationEssentialsTests/ResourceUtilities.swift b/Tests/FoundationEssentialsTests/ResourceUtilities.swift index 5ce04b09f..331f90771 100644 --- a/Tests/FoundationEssentialsTests/ResourceUtilities.swift +++ b/Tests/FoundationEssentialsTests/ResourceUtilities.swift @@ -56,10 +56,23 @@ func testData(forResource resource: String, withExtension ext: String, subdirect // swiftpm drops the resources next to the executable, at: // ./FoundationPreview_FoundationEssentialsTests.resources/Resources/ // Hard-coding the path is unfortunate, but a temporary need until we have a better way to handle this - var path = URL(filePath: ProcessInfo.processInfo.arguments[0]) + + var toolsResourcesDir = URL(filePath: ProcessInfo.processInfo.arguments[0]) .deletingLastPathComponent() - .appending(component: "FoundationPreview_FoundationEssentialsTests.resources", directoryHint: .isDirectory) - .appending(component: "Resources", directoryHint: .isDirectory) + .appending(component: "FoundationPreview_FoundationEssentialsTests-tool.resources", directoryHint: .isDirectory) + + // On Linux the tests are built for the "host" because there are macro tests, on Windows + // the tests are only built for the "target" so we need to figure out whether `-tools` + // resources exist and if so, use them. + let resourcesDir = if FileManager.default.fileExists(atPath: toolsResourcesDir.path) { + toolsResourcesDir + } else { + URL(filePath: ProcessInfo.processInfo.arguments[0]) + .deletingLastPathComponent() + .appending(component: "FoundationPreview_FoundationEssentialsTests.resources", directoryHint: .isDirectory) + } + + var path = resourcesDir.appending(component: "Resources", directoryHint: .isDirectory) if let subdirectory { path.append(path: subdirectory, directoryHint: .isDirectory) } From b0abbb45992f33c23c40f88915456734d41b1dee Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 29 Aug 2024 18:27:06 +0100 Subject: [PATCH 22/34] 6.0: Fix WASI support (#825) * Add explicit include of `wasi/libc-environ.h` (#786) This is necessary to get the `__wasilibc_get_environ` function declaration. (cherry picked from commit 243066f12d0b6f1ab8ad9fefd8526b2383641892) * Add explicit void type parameter to C functions without parameters (#775) C functions with `()` as parameter list can take any number of parameters. But WebAssembly requires static signature information for every function call, so we need to explicitly specify `(void)` to indicate that the function takes no parameters. (cherry picked from commit 8f34f38f30858f7b9dfa9a40b07c18fd7a7b93ae) * Exclude EREMOTE definition for WASI platform (#778) WASI does not define the EREMOTE error code. (cherry picked from commit 6bb5ff7b29ed65a722119057d406ffd4bdcdf1b9) * Throw `.featureUnsupported` when attempting to create temp files on WASI (#779) WASI does not have temp directory concept, and does not provide mktemp family of functions, so attempting to create a temporary file should be considered a feature unsupported. (cherry picked from commit fb11420dc6f546e67fce249c15bb17e208c40aff) * Fix `operatingSystemVersion` on WASI (#782) The `operatingSystemVersion` property type is a tuple but the it was returning an `OperatingSystemVersion` instance on unknown platforms. (cherry picked from commit a8f12255dbf98e0899af84d0e674ab71be30bd85) * Guard out extended or fs attributes related code on WASI (#784) This commit guards out the extended attributes and file system attributes related code on WASI as WASI does not support these features. Just return nothing or ignore the set request. (cherry picked from commit fab7195ea2503d296ce80ff6bc5cdb6da8c71b9c) * Guard out user/group related code on WASI (#783) * Guard out user/group related code on WASI This change guards out the user/group related code on WASI, as WASI does not have the concept of users or groups. * Throw explicit unsupported error if trying to set user or group on WASI Instead of implicitly ignoring user-given values, we should throw exception to make it clear that those values cannot be set on WASI. (cherry picked from commit 0b3974d35103fd1e3e5213f2cdcefc1fd7fa84f4) * Skip sticky-bit check in `isDeletableFile` on WASI (#785) WASI does not surface the sticky bit and getuid, so we cannot check whether the file is actually deletable before attempting to delete it. (cherry picked from commit e90b6c3f90e52f840fc087b05468f430eae1d05a) * Implement `_copyRegularFile` for WASI without `sendfile` (#787) WASI doesn't have `sendfile`, so we need to implement the copy in user space with `read` and `write`. It's not as efficient as `sendfile`, but it's the best we can do. (cherry picked from commit 2a6afeb50aecc5fdfaa7b739399afff1eca024d1) * Port `LockedState` and `_ThreadLocal` to WASI without any locking (#780) (cherry picked from commit aa68eebebcb8dd16b9636c54e580e0ad32bb57e3) * Add WASI platform conditions for libc imports and word size (#776) * Add `import WASILibc` statements to libc import chains * Declare wasm32 arch as 32-bit environment * Switch to _pointerBitWidth for architecture checks This change switches the architecture checks in Data.swift to use the _pointerBitWidth instead of the arch() checks for consistency with newer platforms. (cherry picked from commit c82d1673eb112ac62e4f770947c8a238a7659163) * Enable wasi-libc emulation features (#777) * Enable wasi-libc emulation features Those features require explicit macro definitions to be enabled, so add them to the package definition. Only affects WASI builds. * Prefer `TARGET_OS_WASI` over `__wasi__` And explain why we need definition checks for `signal.h` and `sys/mman.h` (cherry picked from commit c86692f7e7b6d7cb1625d66ee6ff2618011f22f1) --------- Co-authored-by: Yuta Saito --- CMakeLists.txt | 8 +++++ Package.swift | 35 ++++++++++++++----- Sources/FoundationEssentials/CMakeLists.txt | 1 + .../Calendar/Calendar.swift | 2 ++ .../Calendar/Calendar_Gregorian.swift | 2 ++ .../Data/Data+Reading.swift | 2 ++ .../Data/Data+Writing.swift | 9 +++++ Sources/FoundationEssentials/Data/Data.swift | 14 ++++---- Sources/FoundationEssentials/Date.swift | 2 ++ .../Decimal/Decimal+Math.swift | 2 ++ .../Error/CocoaError+FilePath.swift | 2 ++ .../Error/ErrorCodes+POSIX.swift | 4 +++ .../FileManager/FileManager+Basics.swift | 2 ++ .../FileManager/FileManager+Directories.swift | 2 ++ .../FileManager/FileManager+Files.swift | 23 ++++++++++-- .../FileManager+SymbolicLinks.swift | 2 ++ .../FileManager/FileManager+Utilities.swift | 6 ++-- .../FileManager/FileOperations.swift | 23 ++++++++++++ ...yInteger+NumericStringRepresentation.swift | 2 ++ .../FoundationEssentials/LockedState.swift | 9 +++++ Sources/FoundationEssentials/Platform.swift | 4 +-- .../ProcessInfo/ProcessInfo.swift | 4 ++- .../PropertyList/OpenStepPlist.swift | 2 ++ .../String/String+Path.swift | 4 +++ .../FoundationEssentials/_ThreadLocal.swift | 8 +++++ .../CMakeLists.txt | 1 + .../Calendar/Calendar_ICU.swift | 2 ++ .../Formatting/Duration+Formatting.swift | 2 ++ Sources/_FoundationCShims/include/_CStdlib.h | 16 ++++++++- .../include/platform_shims.h | 12 +++---- Sources/_FoundationCShims/platform_shims.c | 20 ++++++----- 31 files changed, 189 insertions(+), 38 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a8dc410cf..3243e5398 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,6 +116,14 @@ foreach(version ${_SwiftFoundation_versions}) endforeach() endforeach() +# wasi-libc emulation feature flags +set(_SwiftFoundation_wasi_libc_flags) +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + list(APPEND _SwiftFoundation_wasi_libc_flags + "SHELL:$<$:-Xcc -D_WASI_EMULATED_SIGNAL>" + "SHELL:$<$:-Xcc -D_WASI_EMULATED_MMAN>") +endif() + include(GNUInstallDirs) include(SwiftFoundationSwiftSupport) diff --git a/Package.swift b/Package.swift index daaf6389b..67bce500a 100644 --- a/Package.swift +++ b/Package.swift @@ -70,6 +70,11 @@ var dependencies: [Package.Dependency] { } } +let wasiLibcCSettings: [CSetting] = [ + .define("_WASI_EMULATED_SIGNAL", .when(platforms: [.wasi])), + .define("_WASI_EMULATED_MMAN", .when(platforms: [.wasi])), +] + let package = Package( name: "FoundationPreview", platforms: [.macOS("13.3"), .iOS("16.4"), .tvOS("16.4"), .watchOS("9.4")], @@ -91,15 +96,23 @@ let package = Package( path: "Sources/Foundation"), // _FoundationCShims (Internal) - .target(name: "_FoundationCShims", - cSettings: [.define("_CRT_SECURE_NO_WARNINGS", - .when(platforms: [.windows]))]), + .target( + name: "_FoundationCShims", + cSettings: [ + .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])) + ] + wasiLibcCSettings + ), // TestSupport (Internal) - .target(name: "TestSupport", dependencies: [ - "FoundationEssentials", - "FoundationInternationalization", - ], swiftSettings: availabilityMacros + concurrencyChecking), + .target( + name: "TestSupport", + dependencies: [ + "FoundationEssentials", + "FoundationInternationalization", + ], + cSettings: wasiLibcCSettings, + swiftSettings: availabilityMacros + concurrencyChecking + ), // FoundationEssentials .target( @@ -130,11 +143,14 @@ let package = Package( ], cSettings: [ .define("_GNU_SOURCE", .when(platforms: [.linux])) - ], + ] + wasiLibcCSettings, swiftSettings: [ .enableExperimentalFeature("VariadicGenerics"), .enableExperimentalFeature("AccessLevelOnImport") - ] + availabilityMacros + concurrencyChecking + ] + availabilityMacros + concurrencyChecking, + linkerSettings: [ + .linkedLibrary("wasi-emulated-getpid", .when(platforms: [.wasi])), + ] ), .testTarget( name: "FoundationEssentialsTests", @@ -166,6 +182,7 @@ let package = Package( "CMakeLists.txt", "Predicate/CMakeLists.txt" ], + cSettings: wasiLibcCSettings, swiftSettings: [ .enableExperimentalFeature("AccessLevelOnImport") ] + availabilityMacros + concurrencyChecking diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index 6dc5929d4..5525efec6 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -65,6 +65,7 @@ target_compile_options(FoundationEssentials PRIVATE "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend StrictConcurrency>" "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend InferSendableFromCaptures>") target_compile_options(FoundationEssentials PRIVATE ${_SwiftFoundation_availability_macros}) +target_compile_options(FoundationEssentials PRIVATE ${_SwiftFoundation_wasi_libc_flags}) target_compile_options(FoundationEssentials PRIVATE -package-name "SwiftFoundation") target_link_libraries(FoundationEssentials PUBLIC diff --git a/Sources/FoundationEssentials/Calendar/Calendar.swift b/Sources/FoundationEssentials/Calendar/Calendar.swift index 9de55b372..257b742ec 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar.swift @@ -20,6 +20,8 @@ import Glibc import Musl #elseif canImport(CRT) import CRT +#elseif os(WASI) +import WASILibc #endif #if FOUNDATION_FRAMEWORK diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift index 797a8e8b7..8c25c77f6 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift @@ -20,6 +20,8 @@ import Glibc import Musl #elseif canImport(CRT) import CRT +#elseif os(WASI) +import WASILibc #endif diff --git a/Sources/FoundationEssentials/Data/Data+Reading.swift b/Sources/FoundationEssentials/Data/Data+Reading.swift index 2540b14eb..48b95214d 100644 --- a/Sources/FoundationEssentials/Data/Data+Reading.swift +++ b/Sources/FoundationEssentials/Data/Data+Reading.swift @@ -27,6 +27,8 @@ import Musl #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif func _fgetxattr(_ fd: Int32, _ name: UnsafePointer!, _ value: UnsafeMutableRawPointer!, _ size: Int, _ position: UInt32, _ options: Int32) -> Int { diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 1e75b43cf..0256e51ef 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -29,6 +29,8 @@ import Musl #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif #if !NO_FILESYSTEM @@ -129,6 +131,10 @@ private func cleanupTemporaryDirectory(at inPath: String?) { /// Caller is responsible for calling `close` on the `Int32` file descriptor. private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL, prefix: String, options: Data.WritingOptions) throws -> (Int32, String) { +#if os(WASI) + // WASI does not have temp directories + throw CocoaError(.featureUnsupported) +#else var directoryPath = destinationPath if !directoryPath.isEmpty && directoryPath.last! != "/" { directoryPath.append("/") @@ -183,6 +189,7 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL, } } } while true +#endif // os(WASI) } /// Returns `(file descriptor, temporary file path, temporary directory path)` @@ -516,6 +523,7 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint cleanupTemporaryDirectory(at: temporaryDirectoryPath) +#if !os(WASI) // WASI does not support fchmod for now if let mode { // Try to change the mode if the path has not changed. Do our best, but don't report an error. #if FOUNDATION_FRAMEWORK @@ -539,6 +547,7 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint fchmod(fd, mode) #endif } +#endif // os(WASI) } } } diff --git a/Sources/FoundationEssentials/Data/Data.swift b/Sources/FoundationEssentials/Data/Data.swift index 8bded856a..ad3ac42e9 100644 --- a/Sources/FoundationEssentials/Data/Data.swift +++ b/Sources/FoundationEssentials/Data/Data.swift @@ -76,6 +76,8 @@ import Glibc import Musl #elseif canImport(ucrt) import ucrt +#elseif canImport(WASILibc) +import WASILibc #endif #if os(Windows) @@ -580,11 +582,11 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect @usableFromInline @frozen internal struct InlineData : Sendable { -#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le) +#if _pointerBitWidth(_64) @usableFromInline typealias Buffer = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) //len //enum @usableFromInline var bytes: Buffer -#elseif arch(i386) || arch(arm) || arch(arm64_32) +#elseif _pointerBitWidth(_32) @usableFromInline typealias Buffer = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) //len //enum @usableFromInline var bytes: Buffer @@ -615,9 +617,9 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect @inlinable // This is @inlinable as a trivial initializer. init(count: Int = 0) { assert(count <= MemoryLayout.size) -#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le) +#if _pointerBitWidth(_64) bytes = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0)) -#elseif arch(i386) || arch(arm) || arch(arm64_32) +#elseif _pointerBitWidth(_32) bytes = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0)) #else #error ("Unsupported architecture: initialization for Buffer is required for this architecture") @@ -802,9 +804,9 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect } } -#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le) +#if _pointerBitWidth(_64) @usableFromInline internal typealias HalfInt = Int32 -#elseif arch(i386) || arch(arm) || arch(arm64_32) +#elseif _pointerBitWidth(_32) @usableFromInline internal typealias HalfInt = Int16 #else #error ("Unsupported architecture: a definition of half of the pointer sized Int needs to be defined for this architecture") diff --git a/Sources/FoundationEssentials/Date.swift b/Sources/FoundationEssentials/Date.swift index b65066f14..37548e498 100644 --- a/Sources/FoundationEssentials/Date.swift +++ b/Sources/FoundationEssentials/Date.swift @@ -20,6 +20,8 @@ import Glibc import Musl #elseif canImport(WinSDK) import WinSDK +#elseif os(WASI) +import WASILibc #endif #if !FOUNDATION_FRAMEWORK diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift index 7b35f1189..eb344b214 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift @@ -20,6 +20,8 @@ import Glibc import Musl #elseif canImport(CRT) import CRT +#elseif os(WASI) +import WASILibc #endif private let powerOfTen: [Decimal.VariableLengthInteger] = [ diff --git a/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift b/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift index d9b249761..586c781c3 100644 --- a/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift +++ b/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift @@ -24,6 +24,8 @@ import Musl #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif extension CocoaError.Code { diff --git a/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift b/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift index 048cd29b2..e1bfffa3f 100644 --- a/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift +++ b/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift @@ -21,6 +21,8 @@ #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif #if FOUNDATION_FRAMEWORK @@ -467,11 +469,13 @@ extension POSIXError { return .ESTALE } + #if !os(WASI) /// Too many levels of remote in path. public static var EREMOTE: POSIXErrorCode { return .EREMOTE } #endif + #endif #if canImport(Darwin) /// RPC struct is bad. diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift b/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift index 991c5e817..9896b35a4 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift @@ -21,6 +21,8 @@ import Musl #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif #if os(Windows) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift index 0941e5186..f8375b382 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift @@ -28,6 +28,8 @@ import Musl #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif internal import _FoundationCShims diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index b8cd50a4c..9cd97525a 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -29,6 +29,9 @@ internal import _FoundationCShims #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +internal import _FoundationCShims +import WASILibc #endif extension Date { @@ -471,7 +474,7 @@ extension _FileManagerImpl { parent = fileManager.currentDirectoryPath } -#if os(Windows) +#if os(Windows) || os(WASI) return fileManager.isWritableFile(atPath: parent) && fileManager.isWritableFile(atPath: path) #else guard fileManager.isWritableFile(atPath: parent), @@ -494,7 +497,7 @@ extension _FileManagerImpl { #endif } -#if !os(Windows) +#if !os(Windows) && !os(WASI) private func _extendedAttribute(_ key: UnsafePointer, at path: UnsafePointer, followSymlinks: Bool) throws -> Data? { #if canImport(Darwin) var size = getxattr(path, key, nil, 0, 0, followSymlinks ? 0 : XATTR_NOFOLLOW) @@ -648,10 +651,11 @@ extension _FileManagerImpl { var attributes = statAtPath.fileAttributes try? Self._catInfo(for: URL(filePath: path, directoryHint: .isDirectory), statInfo: statAtPath, into: &attributes) - + #if !os(WASI) // WASI does not support extended attributes if let extendedAttrs = try? _extendedAttributes(at: fsRep, followSymlinks: false) { attributes[._extendedAttributes] = extendedAttrs } + #endif #if !targetEnvironment(simulator) && FOUNDATION_FRAMEWORK if statAtPath.isRegular || statAtPath.isDirectory { @@ -713,6 +717,9 @@ extension _FileManagerImpl { ] } } +#elseif os(WASI) + // WASI does not support file system attributes + return [:] #else try fileManager.withFileSystemRepresentation(for: path) { rep in guard let rep else { @@ -928,6 +935,10 @@ extension _FileManagerImpl { let groupID = _readFileAttributePrimitive(attributes[.groupOwnerAccountID], as: UInt.self) if user != nil || userID != nil || group != nil || groupID != nil { + #if os(WASI) + // WASI does not have the concept of users or groups + throw CocoaError.errorWithFilePath(.featureUnsupported, path) + #else // Bias toward userID & groupID - try to prevent round trips to getpwnam if possible. var leaveUnchanged: UInt32 { UInt32(bitPattern: -1) } let rawUserID = userID.flatMap(uid_t.init) ?? user.flatMap(Self._userAccountNameToNumber) ?? leaveUnchanged @@ -935,12 +946,18 @@ extension _FileManagerImpl { if chown(fileSystemRepresentation, rawUserID, rawGroupID) != 0 { throw CocoaError.errorWithFilePath(path, errno: errno, reading: false) } + #endif } try Self._setCatInfoAttributes(attributes, path: path) if let extendedAttrs = attributes[.init("NSFileExtendedAttributes")] as? [String : Data] { + #if os(WASI) + // WASI does not support extended attributes + throw CocoaError.errorWithFilePath(.featureUnsupported, path) + #else try Self._setAttributes(extendedAttrs, at: fileSystemRepresentation, followSymLinks: false) + #endif } if let date = attributes[.modificationDate] as? Date { diff --git a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift index a1355e78d..12d32e578 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift @@ -23,6 +23,8 @@ import Musl import CRT import WinSDK internal import _FoundationCShims +#elseif os(WASI) +import WASILibc #endif extension _FileManagerImpl { diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift index 9bac9676f..036f50cc5 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift @@ -34,6 +34,8 @@ internal import _FoundationCShims #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif #if os(Windows) @@ -176,7 +178,7 @@ extension _FileManagerImpl { #endif } -#if !os(Windows) +#if !os(Windows) && !os(WASI) static func _setAttribute(_ key: UnsafePointer, value: Data, at path: UnsafePointer, followSymLinks: Bool) throws { try value.withUnsafeBytes { buffer in #if canImport(Darwin) @@ -274,7 +276,7 @@ extension _FileManagerImpl { } #endif -#if !os(Windows) +#if !os(Windows) && !os(WASI) static func _userAccountNameToNumber(_ name: String) -> uid_t? { name.withCString { ptr in getpwnam(ptr)?.pointee.pw_uid diff --git a/Sources/FoundationEssentials/FileManager/FileOperations.swift b/Sources/FoundationEssentials/FileManager/FileOperations.swift index 03adcc6fa..14c6fd81d 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations.swift @@ -21,6 +21,8 @@ import Musl #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif #if FOUNDATION_FRAMEWORK @@ -866,12 +868,14 @@ enum _FileOperations { } defer { close(dstfd) } + #if !os(WASI) // WASI doesn't have fchmod for now // Set the file permissions using fchmod() instead of when open()ing to avoid umask() issues let permissions = fileInfo.st_mode & ~S_IFMT guard fchmod(dstfd, permissions) == 0 else { try delegate.throwIfNecessary(errno, String(cString: srcPtr), String(cString: dstPtr)) return } + #endif if fileInfo.st_size == 0 { // no copying required @@ -882,12 +886,31 @@ enum _FileOperations { let chunkSize: Int = Int(fileInfo.st_blksize) var current: off_t = 0 + #if os(WASI) + // WASI doesn't have sendfile, so we need to do it in user space with read/write + try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: chunkSize) { buffer in + while current < total { + let readSize = Swift.min(total - Int(current), chunkSize) + let bytesRead = read(srcfd, buffer.baseAddress, readSize) + guard bytesRead >= 0 else { + try delegate.throwIfNecessary(errno, String(cString: srcPtr), String(cString: dstPtr)) + return + } + guard write(dstfd, buffer.baseAddress, bytesRead) == bytesRead else { + try delegate.throwIfNecessary(errno, String(cString: srcPtr), String(cString: dstPtr)) + return + } + current += off_t(bytesRead) + } + } + #else while current < total { guard sendfile(dstfd, srcfd, ¤t, Swift.min(total - Int(current), chunkSize)) != -1 else { try delegate.throwIfNecessary(errno, String(cString: srcPtr), String(cString: dstPtr)) return } } + #endif } #endif diff --git a/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift b/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift index 43e9fcdaa..663509deb 100644 --- a/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift +++ b/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift @@ -20,6 +20,8 @@ import Glibc import Musl #elseif os(Windows) import CRT +#elseif os(WASI) +import WASILibc #endif // MARK: - BinaryInteger + Numeric string representation diff --git a/Sources/FoundationEssentials/LockedState.swift b/Sources/FoundationEssentials/LockedState.swift index 6eb9ad840..4e6aefa8a 100644 --- a/Sources/FoundationEssentials/LockedState.swift +++ b/Sources/FoundationEssentials/LockedState.swift @@ -35,6 +35,9 @@ package struct LockedState { typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK +#elseif os(WASI) + // WASI is single-threaded, so we don't need a lock. + typealias Primitive = Void #endif typealias PlatformLock = UnsafeMutablePointer @@ -47,6 +50,8 @@ package struct LockedState { pthread_mutex_init(platformLock, nil) #elseif canImport(WinSDK) InitializeSRWLock(platformLock) +#elseif os(WASI) + // no-op #endif } @@ -64,6 +69,8 @@ package struct LockedState { pthread_mutex_lock(platformLock) #elseif canImport(WinSDK) AcquireSRWLockExclusive(platformLock) +#elseif os(WASI) + // no-op #endif } @@ -74,6 +81,8 @@ package struct LockedState { pthread_mutex_unlock(platformLock) #elseif canImport(WinSDK) ReleaseSRWLockExclusive(platformLock) +#elseif os(WASI) + // no-op #endif } } diff --git a/Sources/FoundationEssentials/Platform.swift b/Sources/FoundationEssentials/Platform.swift index 9c3f2d7a3..4549a4524 100644 --- a/Sources/FoundationEssentials/Platform.swift +++ b/Sources/FoundationEssentials/Platform.swift @@ -114,7 +114,7 @@ private let _cachedUGIDs: (uid_t, gid_t) = { }() #endif -#if !os(Windows) +#if !os(Windows) && !os(WASI) extension Platform { private static var ROOT_USER: UInt32 { 0 } static func getUGIDs(allowEffectiveRootUID: Bool = true) -> (uid: UInt32, gid: UInt32) { @@ -175,7 +175,7 @@ extension Platform { // FIXME: bionic implements this as `return 0;` and does not expose the // function via headers. We should be able to shim this and use the call // if it is available. -#if !os(Android) +#if !os(Android) && !os(WASI) guard issetugid() == 0 else { return nil } #endif if let value = getenv(name) { diff --git a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift index 2e809fa70..eb55c273d 100644 --- a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift +++ b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift @@ -23,6 +23,8 @@ import Glibc import Musl #elseif os(Windows) import WinSDK +#elseif os(WASI) +import WASILibc #endif #if !NO_PROCESS @@ -391,7 +393,7 @@ extension _ProcessInfo { patch: Int(osVersionInfo.dwBuildNumber) ) #else - return OperatingSystemVersion(majorVersion: -1, minorVersion: 0, patchVersion: 0) + return (major: -1, minor: 0, patch: 0) #endif } diff --git a/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift b/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift index c0428202d..a484557c2 100644 --- a/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift +++ b/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift @@ -16,6 +16,8 @@ import Darwin import Bionic #elseif canImport(Glibc) import Glibc +#elseif os(WASI) +import WASILibc #elseif canImport(Musl) import Musl #endif diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 477d5d39b..7047b9be8 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -20,6 +20,8 @@ import Glibc import Musl #elseif os(Windows) import WinSDK +#elseif os(WASI) +import WASILibc #endif internal import _FoundationCShims @@ -452,6 +454,7 @@ extension String { return envVar.standardizingPath } + #if !os(WASI) // WASI does not have user concept // Next, attempt to find the home directory via getpwnam/getpwuid var pass: UnsafeMutablePointer? if let user { @@ -465,6 +468,7 @@ extension String { if let dir = pass?.pointee.pw_dir { return String(cString: dir).standardizingPath } + #endif // Fallback to HOME for the current user if possible if user == nil, let home = getenv("HOME") { diff --git a/Sources/FoundationEssentials/_ThreadLocal.swift b/Sources/FoundationEssentials/_ThreadLocal.swift index aea9c4116..ffe010c93 100644 --- a/Sources/FoundationEssentials/_ThreadLocal.swift +++ b/Sources/FoundationEssentials/_ThreadLocal.swift @@ -32,6 +32,8 @@ struct _ThreadLocal { fileprivate typealias PlatformKey = tss_t #elseif canImport(WinSDK) fileprivate typealias PlatformKey = DWORD +#elseif os(WASI) + fileprivate typealias PlatformKey = UnsafeMutablePointer #endif struct Key { @@ -48,6 +50,8 @@ struct _ThreadLocal { self.key = key #elseif canImport(WinSDK) key = FlsAlloc(nil) +#elseif os(WASI) + key = UnsafeMutablePointer.allocate(capacity: 1) #endif } } @@ -60,6 +64,8 @@ struct _ThreadLocal { tss_get(key) #elseif canImport(WinSDK) FlsGetValue(key) +#elseif os(WASI) + key.pointee #endif } @@ -70,6 +76,8 @@ struct _ThreadLocal { tss_set(key, newValue) #elseif canImport(WinSDK) FlsSetValue(key, newValue) +#elseif os(WASI) + key.pointee = newValue #endif } } diff --git a/Sources/FoundationInternationalization/CMakeLists.txt b/Sources/FoundationInternationalization/CMakeLists.txt index 5a89ceb88..857db9cac 100644 --- a/Sources/FoundationInternationalization/CMakeLists.txt +++ b/Sources/FoundationInternationalization/CMakeLists.txt @@ -33,6 +33,7 @@ target_compile_options(FoundationInternationalization PRIVATE "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend StrictConcurrency>" "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend InferSendableFromCaptures>") target_compile_options(FoundationInternationalization PRIVATE ${_SwiftFoundation_availability_macros}) +target_compile_options(FoundationInternationalization PRIVATE ${_SwiftFoundation_wasi_libc_flags}) target_compile_options(FoundationInternationalization PRIVATE -package-name "SwiftFoundation") target_link_libraries(FoundationInternationalization PUBLIC diff --git a/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift b/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift index 0d3c3710a..01895b8b9 100644 --- a/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift +++ b/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift @@ -24,6 +24,8 @@ import Musl import CRT #elseif canImport(Darwin) import Darwin +#elseif os(WASI) +import WASILibc #endif internal import _FoundationICU diff --git a/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift b/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift index a94f57161..dfe2fad10 100644 --- a/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift +++ b/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift @@ -24,6 +24,8 @@ import Glibc import Musl #elseif os(Windows) import CRT +#elseif os(WASI) +import WASILibc #endif @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) diff --git a/Sources/_FoundationCShims/include/_CStdlib.h b/Sources/_FoundationCShims/include/_CStdlib.h index 8967eb7f4..03373934b 100644 --- a/Sources/_FoundationCShims/include/_CStdlib.h +++ b/Sources/_FoundationCShims/include/_CStdlib.h @@ -60,7 +60,21 @@ #endif #if __has_include() -#include +/// Guard against including `signal.h` on WASI. The `signal.h` header file +/// itself is available in wasi-libc, but it's just a stub that doesn't actually +/// do anything. And also including it requires a special macro definition +/// (`_WASI_EMULATED_SIGNAL`) and it causes compilation errors without the macro. +# if !TARGET_OS_WASI || defined(_WASI_EMULATED_SIGNAL) +# include +# endif +#endif + +#if __has_include() +/// Similar to `signal.h`, guard against including `sys/mman.h` on WASI unless +/// `_WASI_EMULATED_MMAN` is enabled. +# if !TARGET_OS_WASI || defined(_WASI_EMULATED_MMAN) +# include +# endif #endif #if __has_include() diff --git a/Sources/_FoundationCShims/include/platform_shims.h b/Sources/_FoundationCShims/include/platform_shims.h index 911fc9e7a..f8048e678 100644 --- a/Sources/_FoundationCShims/include/platform_shims.h +++ b/Sources/_FoundationCShims/include/platform_shims.h @@ -31,19 +31,19 @@ #include #endif -INTERNAL char * _Nullable * _Nullable _platform_shims_get_environ(); +INTERNAL char * _Nullable * _Nullable _platform_shims_get_environ(void); -INTERNAL void _platform_shims_lock_environ(); -INTERNAL void _platform_shims_unlock_environ(); +INTERNAL void _platform_shims_lock_environ(void); +INTERNAL void _platform_shims_unlock_environ(void); #if __has_include() #include -INTERNAL vm_size_t _platform_shims_vm_size(); +INTERNAL vm_size_t _platform_shims_vm_size(void); #endif #if __has_include() #include -INTERNAL mach_port_t _platform_mach_task_self(); +INTERNAL mach_port_t _platform_mach_task_self(void); #endif #if __has_include() @@ -65,7 +65,7 @@ typedef enum { } _platform_shims_OSThermalPressureLevel; -INTERNAL const char * _Nonnull _platform_shims_kOSThermalNotificationPressureLevelName(); +INTERNAL const char * _Nonnull _platform_shims_kOSThermalNotificationPressureLevelName(void); #endif #endif /* CSHIMS_PLATFORM_SHIMS */ diff --git a/Sources/_FoundationCShims/platform_shims.c b/Sources/_FoundationCShims/platform_shims.c index 5a400a468..556bc9416 100644 --- a/Sources/_FoundationCShims/platform_shims.c +++ b/Sources/_FoundationCShims/platform_shims.c @@ -21,21 +21,25 @@ extern char **environ; #endif +#if __wasi__ +#include // for __wasilibc_get_environ +#endif + #if __has_include() #import -void _platform_shims_lock_environ() { +void _platform_shims_lock_environ(void) { environ_lock_np(); } -void _platform_shims_unlock_environ() { +void _platform_shims_unlock_environ(void) { environ_unlock_np(); } #else -void _platform_shims_lock_environ() { /* noop */ } -void _platform_shims_unlock_environ() { /* noop */ } +void _platform_shims_lock_environ(void) { /* noop */ } +void _platform_shims_unlock_environ(void) { /* noop */ } #endif -char ** _platform_shims_get_environ() { +char ** _platform_shims_get_environ(void) { #if __has_include() return *_NSGetEnviron(); #elif defined(_WIN32) @@ -48,20 +52,20 @@ char ** _platform_shims_get_environ() { } #if __has_include() -const char * _platform_shims_kOSThermalNotificationPressureLevelName() { +const char * _platform_shims_kOSThermalNotificationPressureLevelName(void) { return kOSThermalNotificationPressureLevelName; } #endif #if __has_include() -vm_size_t _platform_shims_vm_size() { +vm_size_t _platform_shims_vm_size(void) { // This shim exists because vm_page_size is not marked const, and therefore looks like global mutable state to Swift. return vm_page_size; } #endif #if __has_include() -mach_port_t _platform_mach_task_self() { +mach_port_t _platform_mach_task_self(void) { // This shim exists because mach_task_self_ is not marked const, and therefore looks like global mutable state to Swift. return mach_task_self(); } From 9a07d175f93d86f599cad5170f605a6c25bbcd0a Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Wed, 4 Sep 2024 09:22:57 -0700 Subject: [PATCH 23/34] Set SWIFT_SYSTEM_NAME for macro project in CMake (#896) (#898) --- Sources/FoundationMacros/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/FoundationMacros/CMakeLists.txt b/Sources/FoundationMacros/CMakeLists.txt index 97104767c..7746d7bd6 100644 --- a/Sources/FoundationMacros/CMakeLists.txt +++ b/Sources/FoundationMacros/CMakeLists.txt @@ -26,6 +26,14 @@ endif() project(FoundationMacros LANGUAGES Swift) +if(NOT SWIFT_SYSTEM_NAME) + if(CMAKE_SYSTEM_NAME STREQUAL Darwin) + set(SWIFT_SYSTEM_NAME macosx) + else() + set(SWIFT_SYSTEM_NAME "$") + endif() +endif() + # SwiftSyntax Dependency find_package(SwiftSyntax QUIET) if(NOT SwiftSyntax_FOUND) From d7ce7efeb45f973e3b277d2974f9b6cbb2014e23 Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Thu, 5 Sep 2024 09:15:36 -0700 Subject: [PATCH 24/34] (134231086) URL.host should not return percent-encoded host (#902) --- Sources/FoundationEssentials/URL/URL.swift | 2 +- Tests/FoundationEssentialsTests/URLTests.swift | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index e83c01ab1..22cd44515 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -1188,7 +1188,7 @@ public struct URL: Equatable, Sendable, Hashable { return _url.host } #endif - return host() + return host(percentEncoded: false) } /// Returns the host component of the URL if present, otherwise returns `nil`. diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index a1f7c6b49..1a085d765 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -571,6 +571,11 @@ final class URLTests : XCTestCase { XCTAssertEqual(appended.relativePath, "relative/with:slash") } + func testURLHostRetainsIDNAEncoding() throws { + let url = URL(string: "ftp://user:password@*.xn--poema-9qae5a.com.br:4343/cat.txt")! + XCTAssertEqual(url.host, "*.xn--poema-9qae5a.com.br") + } + func testURLComponentsPercentEncodedUnencodedProperties() throws { var comp = URLComponents() From 3c2c36a0023673a00ba5fe232e60ff17c29b9497 Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Thu, 5 Sep 2024 09:16:01 -0700 Subject: [PATCH 25/34] (134382481) URLComponents: support http(s)+unix schemes (#903) --- .../FoundationEssentials/URL/URLParser.swift | 2 ++ .../FoundationEssentialsTests/URLTests.swift | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Sources/FoundationEssentials/URL/URLParser.swift b/Sources/FoundationEssentials/URL/URLParser.swift index efea6b538..7ea11362c 100644 --- a/Sources/FoundationEssentials/URL/URLParser.swift +++ b/Sources/FoundationEssentials/URL/URLParser.swift @@ -222,6 +222,8 @@ internal struct RFC3986Parser: URLParserProtocol { "addressbook", "contact", "phasset", + "http+unix", + "https+unix", ]) private static func looksLikeIPLiteral(_ host: some StringProtocol) -> Bool { diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 1a085d765..ca2dec76c 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -1036,4 +1036,38 @@ final class URLTests : XCTestCase { XCTAssertEqual(comp.percentEncodedPath, "/my%00path") XCTAssertEqual(comp.path, "/my\u{0}path") } + + func testURLComponentsUnixDomainSocketOverHTTPScheme() { + var comp = URLComponents() + comp.scheme = "http+unix" + comp.host = "/path/to/socket" + comp.path = "/info" + XCTAssertEqual(comp.string, "http+unix://%2Fpath%2Fto%2Fsocket/info") + + comp.scheme = "https+unix" + XCTAssertEqual(comp.string, "https+unix://%2Fpath%2Fto%2Fsocket/info") + + comp.encodedHost = "%2Fpath%2Fto%2Fsocket" + XCTAssertEqual(comp.string, "https+unix://%2Fpath%2Fto%2Fsocket/info") + XCTAssertEqual(comp.encodedHost, "%2Fpath%2Fto%2Fsocket") + XCTAssertEqual(comp.host, "/path/to/socket") + XCTAssertEqual(comp.path, "/info") + + // "/path/to/socket" is not a valid host for schemes + // that IDNA-encode hosts instead of percent-encoding + comp.scheme = "http" + XCTAssertNil(comp.string) + + comp.scheme = "https" + XCTAssertNil(comp.string) + + comp.scheme = "https+unix" + XCTAssertEqual(comp.string, "https+unix://%2Fpath%2Fto%2Fsocket/info") + + // Check that we can parse a percent-encoded http+unix URL string + comp = URLComponents(string: "http+unix://%2Fpath%2Fto%2Fsocket/info")! + XCTAssertEqual(comp.encodedHost, "%2Fpath%2Fto%2Fsocket") + XCTAssertEqual(comp.host, "/path/to/socket") + XCTAssertEqual(comp.path, "/info") + } } From 64f9269ff8ac3ed640608dfc153b84eca8c5de79 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 26 Sep 2024 09:40:21 -0700 Subject: [PATCH 26/34] Fix windows creation of relative symlinks to directories (#931) (#940) * Fix windows creation of relative symlinks to directories * Add additional unit tests * Fix windows test failure --- .../FileManager+SymbolicLinks.swift | 3 +- .../FileManager/FileManagerTests.swift | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift index 12d32e578..5d6c26f89 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift @@ -59,7 +59,8 @@ extension _FileManagerImpl { ) throws { #if os(Windows) var bIsDirectory = false - _ = fileManager.fileExists(atPath: destPath, isDirectory: &bIsDirectory) + let absoluteDestPath = URL(filePath: destPath, relativeTo: URL(filePath: path, directoryHint: .notDirectory)).path + _ = fileManager.fileExists(atPath: absoluteDestPath, isDirectory: &bIsDirectory) try path.withNTPathRepresentation { lpSymlinkFileName in try destPath.withFileSystemRepresentation { diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index a25638ab4..9f5816570 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -395,9 +395,13 @@ final class FileManagerTests : XCTestCase { func testCreateSymbolicLinkAtPath() throws { try FileManagerPlayground { "foo" + Directory("dir") {} }.test { try $0.createSymbolicLink(atPath: "bar", withDestinationPath: "foo") XCTAssertEqual(try $0.destinationOfSymbolicLink(atPath: "bar"), "foo") + + try $0.createSymbolicLink(atPath: "dir_link", withDestinationPath: "dir") + XCTAssertEqual(try $0.destinationOfSymbolicLink(atPath: "dir_link"), "dir") XCTAssertThrowsError(try $0.createSymbolicLink(atPath: "bar", withDestinationPath: "foo")) { XCTAssertEqual(($0 as? CocoaError)?.code, .fileWriteFileExists) @@ -409,6 +413,41 @@ final class FileManagerTests : XCTestCase { XCTAssertEqual(($0 as? CocoaError)?.code, .fileReadUnknown) } } + + try FileManagerPlayground { + Directory("dir") { + Directory("other_dir") { + "file" + } + } + }.test { + // Create a relative symlink to other_dir from within dir (tests windows special dir symlink handling) + try $0.createSymbolicLink(atPath: "dir/link", withDestinationPath: "other_dir") + + // Ensure it is created successfully + XCTAssertEqual(try $0.destinationOfSymbolicLink(atPath: "dir/link"), "other_dir") + XCTAssertEqual(try $0.contentsOfDirectory(atPath: "dir/link"), ["file"]) + + do { + // Second symlink creation with an absolute path + let absolute = URL(filePath: "dir/link2", relativeTo: URL(filePath: $0.currentDirectoryPath, directoryHint: .isDirectory)).path + try $0.createSymbolicLink(atPath: absolute, withDestinationPath: "other_dir") + + // Ensure it is created successfully + XCTAssertEqual(try $0.destinationOfSymbolicLink(atPath: "dir/link2"), "other_dir") + XCTAssertEqual(try $0.contentsOfDirectory(atPath: "dir/link2"), ["file"]) + } + + do { + // And lastly a symlink to an absolute path + let absolute = URL(filePath: "dir/other_dir", relativeTo: URL(filePath: $0.currentDirectoryPath, directoryHint: .isDirectory)).path + try $0.createSymbolicLink(atPath: "dir/link3", withDestinationPath: absolute) + + // Ensure it is created successfully + XCTAssertEqual(try $0.destinationOfSymbolicLink(atPath: "dir/link3"), absolute.withFileSystemRepresentation { String(cString: $0!) }) + XCTAssertEqual(try $0.contentsOfDirectory(atPath: "dir/link3"), ["file"]) + } + } } func testMoveItemAtPathToPath() throws { From 156233324a24a456de593792b84ee9a5e7384e65 Mon Sep 17 00:00:00 2001 From: finagolfin Date: Wed, 2 Oct 2024 22:36:53 +0530 Subject: [PATCH 27/34] [Android] Enable more code and tests (#871) (#923) * [Android] Enable more code and tests while disabling setting extended file attributes and a test creating a hard link, features not normally allowed on Android. * Remove incorrect WASI check --- Sources/FoundationEssentials/Data/Data+Reading.swift | 4 ++-- .../FileManager/FileManager+Files.swift | 2 ++ Sources/FoundationEssentials/Platform.swift | 4 ++-- .../FoundationEssentials/ProcessInfo/ProcessInfo.swift | 6 +++--- .../FoundationEssentials/TimeZone/TimeZone_Cache.swift | 2 +- Tests/FoundationEssentialsTests/DataIOTests.swift | 2 +- Tests/FoundationEssentialsTests/DataTests.swift | 2 +- .../FileManager/FileManagerTests.swift | 9 +++++++++ Tests/FoundationEssentialsTests/PredicateTests.swift | 2 +- Tests/FoundationEssentialsTests/ProcessInfoTests.swift | 4 ++-- 10 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Sources/FoundationEssentials/Data/Data+Reading.swift b/Sources/FoundationEssentials/Data/Data+Reading.swift index 48b95214d..612681d2d 100644 --- a/Sources/FoundationEssentials/Data/Data+Reading.swift +++ b/Sources/FoundationEssentials/Data/Data+Reading.swift @@ -34,7 +34,7 @@ import WASILibc func _fgetxattr(_ fd: Int32, _ name: UnsafePointer!, _ value: UnsafeMutableRawPointer!, _ size: Int, _ position: UInt32, _ options: Int32) -> Int { #if canImport(Darwin) return fgetxattr(fd, name, value, size, position, options) -#elseif canImport(Glibc) || canImport(Musl) +#elseif canImport(Glibc) || canImport(Musl) || canImport(Android) return fgetxattr(fd, name, value, size) #else return -1 @@ -355,7 +355,7 @@ internal func readBytesFromFile(path inPath: PathOrURL, reportProgress: Bool, ma let localProgress = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(fileSize)) : nil if fileSize == 0 { - #if os(Linux) + #if os(Linux) || os(Android) // Linux has some files that may report a size of 0 but actually have contents let chunkSize = 1024 * 4 var buffer = malloc(chunkSize)! diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index 9cd97525a..b5050a18c 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -955,6 +955,8 @@ extension _FileManagerImpl { #if os(WASI) // WASI does not support extended attributes throw CocoaError.errorWithFilePath(.featureUnsupported, path) + #elseif canImport(Android) + // Android doesn't allow setting this for normal apps, so just skip it. #else try Self._setAttributes(extendedAttrs, at: fileSystemRepresentation, followSymLinks: false) #endif diff --git a/Sources/FoundationEssentials/Platform.swift b/Sources/FoundationEssentials/Platform.swift index 4549a4524..0a0aaa393 100644 --- a/Sources/FoundationEssentials/Platform.swift +++ b/Sources/FoundationEssentials/Platform.swift @@ -192,7 +192,7 @@ extension Platform { extension Platform { @discardableResult package static func copyCString(dst: UnsafeMutablePointer, src: UnsafePointer, size: Int) -> Int { - #if canImport(Darwin) + #if canImport(Darwin) || canImport(Android) return strlcpy(dst, src, size) #else // Glibc doesn't support strlcpy @@ -267,7 +267,7 @@ extension Platform { return String(cString: buffer.baseAddress!).standardizingPath #endif } -#elseif os(Linux) +#elseif os(Linux) || os(Android) // For Linux, read /proc/self/exe return try? FileManager.default.destinationOfSymbolicLink( atPath: "/proc/self/exe").standardizingPath diff --git a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift index eb55c273d..bb487c7f5 100644 --- a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift +++ b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift @@ -437,7 +437,7 @@ extension _ProcessInfo { var siInfo = SYSTEM_INFO() GetSystemInfo(&siInfo) return Int(siInfo.dwNumberOfProcessors) -#elseif os(Linux) || os(FreeBSD) +#elseif os(Linux) || os(FreeBSD) || canImport(Android) return Int(sysconf(Int32(_SC_NPROCESSORS_CONF))) #else return 1 @@ -454,7 +454,7 @@ extension _ProcessInfo { return 0 } return Int(count) -#elseif os(Linux) || os(FreeBSD) +#elseif os(Linux) || os(FreeBSD) || canImport(Android) #if os(Linux) if let fsCount = Self.fsCoreCount() { return fsCount @@ -548,7 +548,7 @@ extension _ProcessInfo { return 0 } return totalMemoryKB * 1024 -#elseif os(Linux) || os(FreeBSD) +#elseif os(Linux) || os(FreeBSD) || canImport(Android) var memory = sysconf(Int32(_SC_PHYS_PAGES)) memory *= sysconf(Int32(_SC_PAGESIZE)) return UInt64(memory) diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift index 744d77b3b..efb8d678f 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift @@ -173,7 +173,7 @@ struct TimeZoneCache : Sendable { } } -#if os(Linux) && !os(WASI) +#if os(Linux) // Try localtime tzset() var t = time(nil) diff --git a/Tests/FoundationEssentialsTests/DataIOTests.swift b/Tests/FoundationEssentialsTests/DataIOTests.swift index 5087168ac..6b9ecf3f4 100644 --- a/Tests/FoundationEssentialsTests/DataIOTests.swift +++ b/Tests/FoundationEssentialsTests/DataIOTests.swift @@ -238,7 +238,7 @@ class DataIOTests : XCTestCase { } func test_zeroSizeFile() throws { - #if !os(Linux) + #if !os(Linux) && !os(Android) throw XCTSkip("This test is only applicable on Linux") #else // Some files in /proc report a file size of 0 bytes via a stat call diff --git a/Tests/FoundationEssentialsTests/DataTests.swift b/Tests/FoundationEssentialsTests/DataTests.swift index 7eae4692f..88428bfef 100644 --- a/Tests/FoundationEssentialsTests/DataTests.swift +++ b/Tests/FoundationEssentialsTests/DataTests.swift @@ -1837,7 +1837,7 @@ extension DataTests { } func testEOPNOTSUPP() throws { - #if !canImport(Darwin) && !os(Linux) + #if !canImport(Darwin) && !os(Linux) && !os(Android) throw XCTSkip("POSIXError.Code is not supported on this platform") #else // Opening a socket via open(2) on Darwin can result in the EOPNOTSUPP error code diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 9f5816570..4a61503bd 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -23,6 +23,10 @@ import TestSupport @testable import Foundation #endif +#if canImport(Android) +import Android +#endif + extension FileManager { fileprivate var delegateCaptures: DelegateCaptures { (self.delegate as! CapturingFileManagerDelegate).captures @@ -329,8 +333,13 @@ final class FileManagerTests : XCTestCase { XCTAssertTrue($0.delegateCaptures.isEmpty) try $0.linkItem(atPath: "foo", toPath: "bar") XCTAssertEqual($0.delegateCaptures.shouldLink, [.init("foo", "bar")]) + #if os(Android) // Hard links are not normally allowed on Android. + XCTAssertEqual($0.delegateCaptures.shouldProceedAfterLinkError, [.init("foo", "bar", code: .fileWriteNoPermission)]) + XCTAssertFalse($0.fileExists(atPath: "bar")) + #else XCTAssertEqual($0.delegateCaptures.shouldProceedAfterLinkError, []) XCTAssertTrue($0.fileExists(atPath: "bar")) + #endif } try FileManagerPlayground { diff --git a/Tests/FoundationEssentialsTests/PredicateTests.swift b/Tests/FoundationEssentialsTests/PredicateTests.swift index 340b21a4a..d0972b537 100644 --- a/Tests/FoundationEssentialsTests/PredicateTests.swift +++ b/Tests/FoundationEssentialsTests/PredicateTests.swift @@ -364,7 +364,7 @@ final class PredicateTests: XCTestCase { func testRegex_RegexBuilder() throws { #if !canImport(RegexBuilder) throw XCTSkip("RegexBuilder is unavavailable on this platform") - #elseif !os(Linux) && !FOUNDATION_FRAMEWORK + #elseif !os(Linux) && !os(Android) && !FOUNDATION_FRAMEWORK // Disable this test in swift-foundation macOS CI because of incorrect availability annotations in the StringProcessing module throw XCTSkip("This test is currently disabled on this platform") #else diff --git a/Tests/FoundationEssentialsTests/ProcessInfoTests.swift b/Tests/FoundationEssentialsTests/ProcessInfoTests.swift index 1e73a9fbf..5b2cc36cf 100644 --- a/Tests/FoundationEssentialsTests/ProcessInfoTests.swift +++ b/Tests/FoundationEssentialsTests/ProcessInfoTests.swift @@ -115,7 +115,7 @@ final class ProcessInfoTests : XCTestCase { let expectedMinMajorVersion = 2 #endif XCTAssertGreaterThanOrEqual(version.majorVersion, expectedMinMajorVersion, "Unrealistic major system version") - #elseif os(Windows) || os(Linux) + #elseif os(Windows) || os(Linux) || os(Android) let minVersion = OperatingSystemVersion(majorVersion: 1, minorVersion: 0, patchVersion: 0) XCTAssertTrue(ProcessInfo.processInfo.isOperatingSystemAtLeast(minVersion)) #else @@ -171,7 +171,7 @@ final class ProcessInfoTests : XCTestCase { func testProcessName() { #if FOUNDATION_FRAMEWORK let targetName = "TestHost" -#elseif os(Linux) || os(Windows) +#elseif os(Linux) || os(Windows) || os(Android) let targetName = "FoundationPreviewPackageTests.xctest" #else let targetName = "xctest" From d3d8499bcfea5e1d16060d8a1c1e772b23a606d9 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Fri, 11 Oct 2024 11:18:11 -0700 Subject: [PATCH 28/34] Fix TimeZone.current lookup on Windows (#975) (#978) --- .../TimeZone/TimeZone.swift | 2 +- .../TimeZone/TimeZone_Cache.swift | 26 ++++++++++--------- .../TimeZone/TimeZone_ICU.swift | 24 +++++++++++++++++ Sources/_FoundationCShims/include/_CStdlib.h | 2 ++ 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone.swift b/Sources/FoundationEssentials/TimeZone/TimeZone.swift index d924312e5..09cd1ff9a 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone.swift @@ -390,7 +390,7 @@ extension TimeZone { extension TimeZone { internal static func dataFromTZFile(_ name: String) -> Data { -#if NO_TZFILE +#if NO_TZFILE || os(Windows) return Data() #else let path = TZDIR + "/" + name diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift index efb8d678f..ac61ee394 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift @@ -50,6 +50,12 @@ dynamic package func _timeZoneGMTClass() -> _TimeZoneProtocol.Type { } #endif +#if os(Windows) +dynamic package func _timeZoneIdentifier(forWindowsIdentifier windowsIdentifier: String) -> String? { + nil +} +#endif + /// Singleton which listens for notifications about preference changes for TimeZone and holds cached values for current, fixed time zones, etc. struct TimeZoneCache : Sendable { // MARK: - State @@ -129,18 +135,14 @@ struct TimeZoneCache : Sendable { } #if os(Windows) - let hFile = TZDEFAULT.withCString(encodedAs: UTF16.self) { - CreateFileW($0, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, 0, nil) - } - defer { CloseHandle(hFile) } - let dwSize = GetFinalPathNameByHandleW(hFile, nil, 0, VOLUME_NAME_DOS) - let path = withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwSize)) { - _ = GetFinalPathNameByHandleW(hFile, $0.baseAddress, dwSize, VOLUME_NAME_DOS) - return String(decodingCString: $0.baseAddress!, as: UTF16.self) - } - if let rangeOfZoneInfo = path._range(of: "\(TZDIR)\\", anchored: false, backwards: false) { - let name = path[rangeOfZoneInfo.upperBound...] - if let result = fixed(String(name)) { + var timeZoneInfo = TIME_ZONE_INFORMATION() + if GetTimeZoneInformation(&timeZoneInfo) != TIME_ZONE_ID_INVALID { + let windowsName = withUnsafePointer(to: &(timeZoneInfo.StandardName)) { + $0.withMemoryRebound(to: WCHAR.self, capacity: 32) { + String(decoding: UnsafeBufferPointer(start: $0, count: wcslen($0)), as: UTF16.self) + } + } + if let identifier = _timeZoneIdentifier(forWindowsIdentifier: windowsName), let result = fixed(identifier) { return TimeZone(inner: result) } } diff --git a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift index 8b63f0931..bff665f82 100644 --- a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift +++ b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift @@ -32,6 +32,13 @@ private func _timeZoneICUClass_localized() -> _TimeZoneProtocol.Type? { } #endif +#if os(Windows) +@_dynamicReplacement(for: _timeZoneIdentifier(forWindowsIdentifier:)) +private func _timeZoneIdentifier_ICU(forWindowsIdentifier windowsIdentifier: String) -> String? { + _TimeZoneICU.getSystemTimeZoneID(forWindowsIdentifier: windowsIdentifier) +} +#endif + internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { init?(secondsFromGMT: Int) { fatalError("Unexpected init") @@ -309,6 +316,23 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { return result } + #if os(Windows) + internal static func getSystemTimeZoneID(forWindowsIdentifier identifier: String) -> String? { + let timeZoneIdentifier = Array(identifier.utf16) + let result: String? = timeZoneIdentifier.withUnsafeBufferPointer { identifier in + return _withResizingUCharBuffer { buffer, size, status in + let len = ucal_getTimeZoneIDForWindowsID(identifier.baseAddress, Int32(identifier.count), nil, buffer, size, &status) + if status.isSuccess { + return len + } else { + return nil + } + } + } + return result + } + #endif + internal static func timeZoneNamesFromICU() -> [String] { let filteredTimeZoneNames = [ "ACT", diff --git a/Sources/_FoundationCShims/include/_CStdlib.h b/Sources/_FoundationCShims/include/_CStdlib.h index 03373934b..5232967c9 100644 --- a/Sources/_FoundationCShims/include/_CStdlib.h +++ b/Sources/_FoundationCShims/include/_CStdlib.h @@ -156,6 +156,7 @@ #include #else +#if TARGET_OS_MAC || TARGET_OS_LINUX #ifndef TZDIR #define TZDIR "/usr/share/zoneinfo/" /* Time zone object file directory */ #endif /* !defined TZDIR */ @@ -163,6 +164,7 @@ #ifndef TZDEFAULT #define TZDEFAULT "/etc/localtime" #endif /* !defined TZDEFAULT */ +#endif /* TARGET_OS_MAC || TARGET_OS_LINUX */ #endif From 77f79edf0e3b2a0debb969a2210b3a73c24f336c Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:38:43 -0700 Subject: [PATCH 29/34] [6.0.x] `URL` path bug fixes (#969) * (133878310) URL.fileSystemPath should drop all trailing slashes (#852) * (133882014) URL(filePath: path, directoryHint: .notDirectory) should strip trailing slashes (#867) * (137129292) URL(filePath:) should not treat "~" as absolute (#961) * (137068266) URL.fileSystemPath should strip leading slash for Windows drive letters (#964) * (137287143) URL path extension APIs should strip trailing slashes (#965) --- .../String/String+Path.swift | 32 +++- Sources/FoundationEssentials/URL/URL.swift | 145 ++++++++++-------- .../StringTests.swift | 22 +++ .../FoundationEssentialsTests/URLTests.swift | 104 +++++++++++++ 4 files changed, 234 insertions(+), 69 deletions(-) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 7047b9be8..3657e6fd4 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -194,7 +194,11 @@ extension String { guard let lastDot = self.utf8.lastIndex(of: dot) else { return self } - return String(self[.. Bool { @@ -214,7 +218,16 @@ extension String { guard validatePathExtension(pathExtension) else { return self } - return self + ".\(pathExtension)" + var result = self._droppingTrailingSlashes + guard result != "/" else { + // Path was all slashes + return self + ".\(pathExtension)" + } + result += ".\(pathExtension)" + if utf8.last == ._slash { + result += "/" + } + return result } internal var pathExtension: String { @@ -366,7 +379,7 @@ extension String { return String(cString: output) } - #if !NO_FILESYSTEM +#if !NO_FILESYSTEM internal static func homeDirectoryPath(forUser user: String? = nil) -> String { #if os(Windows) if let user { @@ -529,8 +542,10 @@ extension String { #else return "/tmp/" #endif -#endif +#endif // os(Windows) } +#endif // !NO_FILESYSTEM + /// Replaces any number of sequential `/` /// characters with / /// NOTE: Internal so it's testable @@ -569,7 +584,7 @@ extension String { } } - private var _droppingTrailingSlashes: String { + internal var _droppingTrailingSlashes: String { guard !self.isEmpty else { return self } @@ -579,7 +594,9 @@ extension String { } return String(self[...lastNonSlash]) } - + +#if !NO_FILESYSTEM + static var NETWORK_PREFIX: String { #"\\"# } private var _standardizingPath: String { @@ -616,7 +633,8 @@ extension String { var standardizingPath: String { expandingTildeInPath._standardizingPath } - #endif // !NO_FILESYSTEM + +#endif // !NO_FILESYSTEM // _NSPathComponents var pathComponents: [String] { diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index 22cd44515..ba2f02c79 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -1319,13 +1319,32 @@ public struct URL: Equatable, Sendable, Hashable { } } - private static func fileSystemPath(for urlPath: String) -> String { - var result = urlPath - if result.count > 1 && result.utf8.last == UInt8(ascii: "/") { - _ = result.popLast() + private static func windowsPath(for posixPath: String) -> String { + let utf8 = posixPath.utf8 + guard utf8.count >= 4 else { + return posixPath + } + // "C:\" is standardized to "/C:/" on initialization + let array = Array(utf8) + if array[0] == ._slash, + array[1].isAlpha, + array[2] == ._colon, + array[3] == ._slash { + return String(Substring(utf8.dropFirst())) } + return posixPath + } + + private static func fileSystemPath(for urlPath: String) -> String { let charsToLeaveEncoded: Set = [._slash, 0] - return Parser.percentDecode(result, excluding: charsToLeaveEncoded) ?? "" + guard let posixPath = Parser.percentDecode(urlPath._droppingTrailingSlashes, excluding: charsToLeaveEncoded) else { + return "" + } + #if os(Windows) + return windowsPath(for: posixPath) + #else + return posixPath + #endif } var fileSystemPath: String { @@ -2026,55 +2045,65 @@ extension URL { #if !NO_FILESYSTEM private static func isDirectory(_ path: String) -> Bool { -#if !FOUNDATION_FRAMEWORK + #if os(Windows) + let path = path.replacing(._slash, with: ._backslash) + #endif + #if !FOUNDATION_FRAMEWORK var isDirectory: Bool = false _ = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) return isDirectory -#else + #else var isDirectory: ObjCBool = false _ = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) return isDirectory.boolValue -#endif + #endif } #endif // !NO_FILESYSTEM /// Checks if a file path is absolute and standardizes the inputted file path on Windows + /// Assumes the path only contains `/` as the path separator internal static func isAbsolute(standardizing filePath: inout String) -> Bool { + if filePath.utf8.first == ._slash { + return true + } #if os(Windows) - var isAbsolute = false let utf8 = filePath.utf8 - if utf8.first == ._backslash { - // Either an absolute path or a UNC path - isAbsolute = true - } else if utf8.count >= 3 { - // Check if this is a drive letter - let first = utf8.first! - let secondIndex = utf8.index(after: utf8.startIndex) - let second = utf8[secondIndex] - let thirdIndex = utf8.index(after: secondIndex) - let third = utf8[thirdIndex] - isAbsolute = ( - first.isAlpha - && (second == ._colon || second == ._pipe) - && third == ._backslash - ) - - if isAbsolute { - // Standardize to "\[drive-letter]:\..." - if second == ._pipe { - var filePathArray = Array(utf8) - filePathArray[1] = ._colon - filePathArray.insert(._backslash, at: 0) - filePath = String(decoding: filePathArray, as: UTF8.self) - } else { - filePath = "\\" + filePath - } + guard utf8.count >= 3 else { + return false + } + // Check if this is a drive letter + let first = utf8.first! + let secondIndex = utf8.index(after: utf8.startIndex) + let second = utf8[secondIndex] + let thirdIndex = utf8.index(after: secondIndex) + let third = utf8[thirdIndex] + let isAbsolute = ( + first.isAlpha + && (second == ._colon || second == ._pipe) + && third == ._slash + ) + if isAbsolute { + // Standardize to "/[drive-letter]:/..." + if second == ._pipe { + var filePathArray = Array(utf8) + filePathArray[1] = ._colon + filePathArray.insert(._slash, at: 0) + filePath = String(decoding: filePathArray, as: UTF8.self) + } else { + filePath = "/" + filePath } } - #else - let isAbsolute = filePath.utf8.first == UInt8(ascii: "/") || filePath.utf8.first == UInt8(ascii: "~") - #endif return isAbsolute + #else // os(Windows) + #if !NO_FILESYSTEM + // Expand the tilde if present + if filePath.utf8.first == UInt8(ascii: "~") { + filePath = filePath.expandingTildeInPath + } + #endif + // Make sure the expanded path is absolute + return filePath.utf8.first == ._slash + #endif // os(Windows) } /// Initializes a newly created file URL referencing the local file or directory at path, relative to a base URL. @@ -2111,10 +2140,9 @@ extension URL { } #if os(Windows) - let slash = UInt8(ascii: "\\") - var filePath = path.replacing(UInt8(ascii: "/"), with: slash) + // Convert any "\" to "/" before storing the URL parse info + var filePath = path.replacing(._backslash, with: ._slash) #else - let slash = UInt8(ascii: "/") var filePath = path #endif @@ -2126,41 +2154,31 @@ extension URL { } #endif - func absoluteFilePath() -> String { - guard !isAbsolute, let baseURL else { - return filePath - } - let basePath = baseURL.path() - #if os(Windows) - let urlPath = filePath.replacing(UInt8(ascii: "\\"), with: UInt8(ascii: "/")) - return URL.fileSystemPath(for: basePath.merging(relativePath: urlPath)).replacing(UInt8(ascii: "/"), with: UInt8(ascii: "\\")) - #else - return URL.fileSystemPath(for: basePath.merging(relativePath: filePath)) - #endif - } - let isDirectory: Bool switch directoryHint { case .isDirectory: isDirectory = true case .notDirectory: + filePath = filePath._droppingTrailingSlashes isDirectory = false case .checkFileSystem: #if !NO_FILESYSTEM + func absoluteFilePath() -> String { + guard !isAbsolute, let baseURL else { + return filePath + } + let absolutePath = baseURL.path().merging(relativePath: filePath) + return URL.fileSystemPath(for: absolutePath) + } isDirectory = URL.isDirectory(absoluteFilePath()) #else - isDirectory = filePath.utf8.last == slash + isDirectory = filePath.utf8.last == ._slash #endif case .inferFromPath: - isDirectory = filePath.utf8.last == slash + isDirectory = filePath.utf8.last == ._slash } - #if os(Windows) - // Convert any "\" back to "/" before storing the URL parse info - filePath = filePath.replacing(UInt8(ascii: "\\"), with: UInt8(ascii: "/")) - #endif - - if !filePath.isEmpty && filePath.utf8.last != UInt8(ascii: "/") && isDirectory { + if isDirectory && !filePath.isEmpty && filePath.utf8.last != ._slash { filePath += "/" } var components = URLComponents() @@ -2438,6 +2456,9 @@ extension URL { guard var filePath = path else { return nil } + #if os(Windows) + filePath = filePath.replacing(._backslash, with: ._slash) + #endif guard URL.isAbsolute(standardizing: &filePath) else { return nil } diff --git a/Tests/FoundationEssentialsTests/StringTests.swift b/Tests/FoundationEssentialsTests/StringTests.swift index b5fff9aee..c7d650638 100644 --- a/Tests/FoundationEssentialsTests/StringTests.swift +++ b/Tests/FoundationEssentialsTests/StringTests.swift @@ -812,6 +812,19 @@ final class StringTests : XCTestCase { } } + func testAppendingPathExtension() { + XCTAssertEqual("".appendingPathExtension("foo"), ".foo") + XCTAssertEqual("/".appendingPathExtension("foo"), "/.foo") + XCTAssertEqual("//".appendingPathExtension("foo"), "//.foo") + XCTAssertEqual("/path".appendingPathExtension("foo"), "/path.foo") + XCTAssertEqual("/path.zip".appendingPathExtension("foo"), "/path.zip.foo") + XCTAssertEqual("/path/".appendingPathExtension("foo"), "/path.foo/") + XCTAssertEqual("/path//".appendingPathExtension("foo"), "/path.foo/") + XCTAssertEqual("path".appendingPathExtension("foo"), "path.foo") + XCTAssertEqual("path/".appendingPathExtension("foo"), "path.foo/") + XCTAssertEqual("path//".appendingPathExtension("foo"), "path.foo/") + } + func testDeletingPathExtenstion() { XCTAssertEqual("".deletingPathExtension(), "") XCTAssertEqual("/".deletingPathExtension(), "/") @@ -834,6 +847,15 @@ final class StringTests : XCTestCase { XCTAssertEqual("/foo.bar/bar.baz/baz.zip".deletingPathExtension(), "/foo.bar/bar.baz/baz") XCTAssertEqual("/.././.././a.zip".deletingPathExtension(), "/.././.././a") XCTAssertEqual("/.././.././.".deletingPathExtension(), "/.././.././.") + + XCTAssertEqual("path.foo".deletingPathExtension(), "path") + XCTAssertEqual("path.foo.zip".deletingPathExtension(), "path.foo") + XCTAssertEqual("/path.foo".deletingPathExtension(), "/path") + XCTAssertEqual("/path.foo.zip".deletingPathExtension(), "/path.foo") + XCTAssertEqual("path.foo/".deletingPathExtension(), "path/") + XCTAssertEqual("path.foo//".deletingPathExtension(), "path/") + XCTAssertEqual("/path.foo/".deletingPathExtension(), "/path/") + XCTAssertEqual("/path.foo//".deletingPathExtension(), "/path/") } func test_dataUsingEncoding() { diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index ca2dec76c..d00ef806d 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -330,6 +330,18 @@ final class URLTests : XCTestCase { try FileManager.default.removeItem(at: URL(filePath: "\(tempDirectory.path)/tmp-dir")) } + #if os(Windows) + func testURLWindowsDriveLetterPath() throws { + let url = URL(filePath: "C:\\test\\path", directoryHint: .notDirectory) + // .absoluteString and .path() use the RFC 8089 URL path + XCTAssertEqual(url.absoluteString, "file:///C:/test/path") + XCTAssertEqual(url.path(), "/C:/test/path") + // .path and .fileSystemPath strip the leading slash + XCTAssertEqual(url.path, "C:/test/path") + XCTAssertEqual(url.fileSystemPath, "C:/test/path") + } + #endif + func testURLFilePathRelativeToBase() throws { try FileManagerPlayground { Directory("dir") { @@ -571,11 +583,103 @@ final class URLTests : XCTestCase { XCTAssertEqual(appended.relativePath, "relative/with:slash") } + func testURLFilePathDropsTrailingSlashes() throws { + var url = URL(filePath: "/path/slashes///") + XCTAssertEqual(url.path(), "/path/slashes///") + // TODO: Update this once .fileSystemPath uses backslashes for Windows + XCTAssertEqual(url.fileSystemPath, "/path/slashes") + + url = URL(filePath: "/path/slashes/") + XCTAssertEqual(url.path(), "/path/slashes/") + XCTAssertEqual(url.fileSystemPath, "/path/slashes") + + url = URL(filePath: "/path/slashes") + XCTAssertEqual(url.path(), "/path/slashes") + XCTAssertEqual(url.fileSystemPath, "/path/slashes") + } + + func testURLNotDirectoryHintStripsTrailingSlash() throws { + // Supply a path with a trailing slash but say it's not a direcotry + var url = URL(filePath: "/path/", directoryHint: .notDirectory) + XCTAssertFalse(url.hasDirectoryPath) + XCTAssertEqual(url.path(), "/path") + + url = URL(fileURLWithPath: "/path/", isDirectory: false) + XCTAssertFalse(url.hasDirectoryPath) + XCTAssertEqual(url.path(), "/path") + + url = URL(filePath: "/path///", directoryHint: .notDirectory) + XCTAssertFalse(url.hasDirectoryPath) + XCTAssertEqual(url.path(), "/path") + + url = URL(fileURLWithPath: "/path///", isDirectory: false) + XCTAssertFalse(url.hasDirectoryPath) + XCTAssertEqual(url.path(), "/path") + + // With .checkFileSystem, don't modify the path for a non-existent file + url = URL(filePath: "/my/non/existent/path/", directoryHint: .checkFileSystem) + XCTAssertTrue(url.hasDirectoryPath) + XCTAssertEqual(url.path(), "/my/non/existent/path/") + + url = URL(fileURLWithPath: "/my/non/existent/path/") + XCTAssertTrue(url.hasDirectoryPath) + XCTAssertEqual(url.path(), "/my/non/existent/path/") + + url = URL(filePath: "/my/non/existent/path", directoryHint: .checkFileSystem) + XCTAssertFalse(url.hasDirectoryPath) + XCTAssertEqual(url.path(), "/my/non/existent/path") + + url = URL(fileURLWithPath: "/my/non/existent/path") + XCTAssertFalse(url.hasDirectoryPath) + XCTAssertEqual(url.path(), "/my/non/existent/path") + } + func testURLHostRetainsIDNAEncoding() throws { let url = URL(string: "ftp://user:password@*.xn--poema-9qae5a.com.br:4343/cat.txt")! XCTAssertEqual(url.host, "*.xn--poema-9qae5a.com.br") } + func testURLTildeFilePath() throws { + var url = URL(filePath: "~") + // "~" must either be expanded to an absolute path or resolved against a base URL + XCTAssertTrue( + url.relativePath.utf8.first == ._slash || (url.baseURL != nil && url.path().utf8.first == ._slash) + ) + + url = URL(filePath: "~", directoryHint: .isDirectory) + XCTAssertTrue( + url.relativePath.utf8.first == ._slash || (url.baseURL != nil && url.path().utf8.first == ._slash) + ) + XCTAssertEqual(url.path().utf8.last, ._slash) + + url = URL(filePath: "~/") + XCTAssertTrue( + url.relativePath.utf8.first == ._slash || (url.baseURL != nil && url.path().utf8.first == ._slash) + ) + XCTAssertEqual(url.path().utf8.last, ._slash) + } + + func testURLPathExtensions() throws { + var url = URL(filePath: "/path", directoryHint: .notDirectory) + url.appendPathExtension("foo") + XCTAssertEqual(url.path(), "/path.foo") + url.deletePathExtension() + XCTAssertEqual(url.path(), "/path") + + url = URL(filePath: "/path", directoryHint: .isDirectory) + url.appendPathExtension("foo") + XCTAssertEqual(url.path(), "/path.foo/") + url.deletePathExtension() + XCTAssertEqual(url.path(), "/path/") + + url = URL(filePath: "/path/", directoryHint: .inferFromPath) + url.appendPathExtension("foo") + XCTAssertEqual(url.path(), "/path.foo/") + url.append(path: "/////") + url.deletePathExtension() + XCTAssertEqual(url.path(), "/path/") + } + func testURLComponentsPercentEncodedUnencodedProperties() throws { var comp = URLComponents() From 0c90c25db4379c390cea3be8becb05cbaa4dd9f3 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Wed, 23 Oct 2024 10:36:36 -0700 Subject: [PATCH 30/34] Fetching user/group info causes race conditions (#994) (#998) * Avoid racy stdlib functions for fetching user/group info * Refactor naming * Fix build failure --- .../FileManager/FileManager+Files.swift | 24 ++----- .../FileManager/FileManager+Utilities.swift | 14 ---- .../FileManager+DarwinSearchPaths.swift | 18 ++---- Sources/FoundationEssentials/Platform.swift | 64 +++++++++++++++++++ .../ProcessInfo/ProcessInfo.swift | 10 ++- .../String/String+Path.swift | 13 ++-- .../StringTests.swift | 11 ---- 7 files changed, 83 insertions(+), 71 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index b5050a18c..4b628c8da 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -41,22 +41,6 @@ extension Date { } #if !os(Windows) -#if !os(WASI) -private func _nameFor(uid: uid_t) -> String? { - guard let pwd = getpwuid(uid), let name = pwd.pointee.pw_name else { - return nil - } - return String(cString: name) -} - -private func _nameFor(gid: gid_t) -> String? { - guard let pwd = getgrgid(gid), let name = pwd.pointee.gr_name else { - return nil - } - return String(cString: name) -} -#endif - extension mode_t { private var _fileType: FileAttributeType { switch self & S_IFMT { @@ -192,10 +176,10 @@ extension stat { .groupOwnerAccountID : _writeFileAttributePrimitive(st_gid, as: UInt.self) ] #if !os(WASI) - if let userName = _nameFor(uid: st_uid) { + if let userName = Platform.name(forUID: st_uid) { result[.ownerAccountName] = userName } - if let groupName = _nameFor(gid: st_gid) { + if let groupName = Platform.name(forGID: st_gid) { result[.groupOwnerAccountName] = groupName } #endif @@ -941,8 +925,8 @@ extension _FileManagerImpl { #else // Bias toward userID & groupID - try to prevent round trips to getpwnam if possible. var leaveUnchanged: UInt32 { UInt32(bitPattern: -1) } - let rawUserID = userID.flatMap(uid_t.init) ?? user.flatMap(Self._userAccountNameToNumber) ?? leaveUnchanged - let rawGroupID = groupID.flatMap(gid_t.init) ?? group.flatMap(Self._groupAccountNameToNumber) ?? leaveUnchanged + let rawUserID = userID.flatMap(uid_t.init) ?? user.flatMap(Platform.uid(forName:)) ?? leaveUnchanged + let rawGroupID = groupID.flatMap(gid_t.init) ?? group.flatMap(Platform.gid(forName:)) ?? leaveUnchanged if chown(fileSystemRepresentation, rawUserID, rawGroupID) != 0 { throw CocoaError.errorWithFilePath(path, errno: errno, reading: false) } diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift index 036f50cc5..525bef766 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift @@ -275,20 +275,6 @@ extension _FileManagerImpl { } } #endif - -#if !os(Windows) && !os(WASI) - static func _userAccountNameToNumber(_ name: String) -> uid_t? { - name.withCString { ptr in - getpwnam(ptr)?.pointee.pw_uid - } - } - - static func _groupAccountNameToNumber(_ name: String) -> gid_t? { - name.withCString { ptr in - getgrnam(ptr)?.pointee.gr_gid - } - } -#endif } extension FileManager { diff --git a/Sources/FoundationEssentials/FileManager/SearchPaths/FileManager+DarwinSearchPaths.swift b/Sources/FoundationEssentials/FileManager/SearchPaths/FileManager+DarwinSearchPaths.swift index f34d1bfe5..8aa397f97 100644 --- a/Sources/FoundationEssentials/FileManager/SearchPaths/FileManager+DarwinSearchPaths.swift +++ b/Sources/FoundationEssentials/FileManager/SearchPaths/FileManager+DarwinSearchPaths.swift @@ -160,20 +160,12 @@ extension String { guard self == "~" || self.hasPrefix("~/") else { return self } - var bufSize = sysconf(_SC_GETPW_R_SIZE_MAX) - if bufSize == -1 { - bufSize = 4096 // A generous guess. - } - return withUnsafeTemporaryAllocation(of: CChar.self, capacity: bufSize) { pwBuff in - var pw: UnsafeMutablePointer? - var pwd = passwd() - let euid = geteuid() - let trueUid = euid == 0 ? getuid() : euid - guard getpwuid_r(trueUid, &pwd, pwBuff.baseAddress!, bufSize, &pw) == 0, let pw else { - return self - } - return String(cString: pw.pointee.pw_dir).appendingPathComponent(String(self.dropFirst())) + let euid = geteuid() + let trueUid = euid == 0 ? getuid() : euid + guard let name = Platform.name(forUID: trueUid) else { + return self } + return name.appendingPathComponent(String(self.dropFirst())) } } #endif // os(macOS) && FOUNDATION_FRAMEWORK diff --git a/Sources/FoundationEssentials/Platform.swift b/Sources/FoundationEssentials/Platform.swift index 0a0aaa393..6eb3accc4 100644 --- a/Sources/FoundationEssentials/Platform.swift +++ b/Sources/FoundationEssentials/Platform.swift @@ -135,6 +135,70 @@ extension Platform { } return result } + + #if canImport(Darwin) + typealias Operation = (Input, UnsafeMutablePointer?, UnsafeMutablePointer?, Int, UnsafeMutablePointer?>?) -> Int32 + #else + typealias Operation = (Input, UnsafeMutablePointer, UnsafeMutablePointer, Int, UnsafeMutablePointer?>) -> Int32 + #endif + + private static func withUserGroupBuffer(_ input: Input, _ output: Output, sizeProperty: Int32, operation: Operation, block: (Output) throws -> R) rethrows -> R? { + var bufferLen = sysconf(sizeProperty) + if bufferLen == -1 { + bufferLen = 4096 // Generous default size estimate + } + return try withUnsafeTemporaryAllocation(of: CChar.self, capacity: bufferLen) { + var result = output + var ptr: UnsafeMutablePointer? + let error = operation(input, &result, $0.baseAddress!, bufferLen, &ptr) + guard error == 0 && ptr != nil else { + return nil + } + return try block(result) + } + } + + static func uid(forName name: String) -> uid_t? { + withUserGroupBuffer(name, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwnam_r) { + $0.pw_uid + } + } + + static func gid(forName name: String) -> uid_t? { + withUserGroupBuffer(name, group(), sizeProperty: Int32(_SC_GETGR_R_SIZE_MAX), operation: getgrnam_r) { + $0.gr_gid + } + } + + static func name(forUID uid: uid_t) -> String? { + withUserGroupBuffer(uid, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwuid_r) { + String(cString: $0.pw_name) + } + } + + static func fullName(forUID uid: uid_t) -> String? { + withUserGroupBuffer(uid, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwuid_r) { + String(cString: $0.pw_gecos) + } + } + + static func name(forGID gid: gid_t) -> String? { + withUserGroupBuffer(gid, group(), sizeProperty: Int32(_SC_GETGR_R_SIZE_MAX), operation: getgrgid_r) { + String(cString: $0.gr_name) + } + } + + static func homeDirectory(forUserName userName: String) -> String? { + withUserGroupBuffer(userName, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwnam_r) { + String(cString: $0.pw_dir) + } + } + + static func homeDirectory(forUID uid: uid_t) -> String? { + withUserGroupBuffer(uid, passwd(), sizeProperty: Int32(_SC_GETPW_R_SIZE_MAX), operation: getpwuid_r) { + String(cString: $0.pw_dir) + } + } } #endif diff --git a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift index bb487c7f5..485f60681 100644 --- a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift +++ b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift @@ -168,9 +168,8 @@ final class _ProcessInfo: Sendable { #if canImport(Darwin) || os(Android) || canImport(Glibc) || canImport(Musl) // Darwin and Linux let (euid, _) = Platform.getUGIDs() - if let upwd = getpwuid(euid), - let uname = upwd.pointee.pw_name { - return String(cString: uname) + if let username = Platform.name(forUID: euid) { + return username } else if let username = self.environment["USER"] { return username } @@ -202,9 +201,8 @@ final class _ProcessInfo: Sendable { var fullUserName: String { #if canImport(Darwin) || os(Android) || canImport(Glibc) || canImport(Musl) let (euid, _) = Platform.getUGIDs() - if let upwd = getpwuid(euid), - let fullname = upwd.pointee.pw_gecos { - return String(cString: fullname) + if let fullName = Platform.fullName(forUID: euid) { + return fullName } return "" #elseif os(WASI) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 3657e6fd4..1f86bb41a 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -469,17 +469,16 @@ extension String { #if !os(WASI) // WASI does not have user concept // Next, attempt to find the home directory via getpwnam/getpwuid - var pass: UnsafeMutablePointer? if let user { - pass = getpwnam(user) + if let dir = Platform.homeDirectory(forUserName: user) { + return dir.standardizingPath + } } else { // We use the real UID instead of the EUID here when the EUID is the root user (i.e. a process has called seteuid(0)) // In this instance, we historically do this to ensure a stable home directory location for processes that call seteuid(0) - pass = getpwuid(Platform.getUGIDs(allowEffectiveRootUID: false).uid) - } - - if let dir = pass?.pointee.pw_dir { - return String(cString: dir).standardizingPath + if let dir = Platform.homeDirectory(forUID: Platform.getUGIDs(allowEffectiveRootUID: false).uid) { + return dir.standardizingPath + } } #endif diff --git a/Tests/FoundationEssentialsTests/StringTests.swift b/Tests/FoundationEssentialsTests/StringTests.swift index c7d650638..ec412cce8 100644 --- a/Tests/FoundationEssentialsTests/StringTests.swift +++ b/Tests/FoundationEssentialsTests/StringTests.swift @@ -2526,17 +2526,6 @@ final class StringTestsStdlib: XCTestCase { expectTrue(availableEncodings.contains("abc".smallestEncoding)) } - func getHomeDir() -> String { -#if os(macOS) - return String(cString: getpwuid(getuid()).pointee.pw_dir) -#elseif canImport(Darwin) - // getpwuid() returns null in sandboxed apps under iOS simulator. - return NSHomeDirectory() -#else - preconditionFailed("implement") -#endif - } - func test_addingPercentEncoding() { expectEqual( "abcd1234", From 201e2d25b1e14195768e46e50f51b44885b6e4fe Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:02:23 -0600 Subject: [PATCH 31/34] (138059051) URL: Appending to an empty file path results in an absolute path (#988) (#1019) --- Sources/FoundationEssentials/URL/URL.swift | 30 +++++-- .../FoundationEssentialsTests/URLTests.swift | 88 ++++++++++++++----- 2 files changed, 89 insertions(+), 29 deletions(-) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index ba2f02c79..063d5f9f3 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -2203,9 +2203,8 @@ extension URL { var path = String(path) #endif - var newPath = relativePath() var insertedSlash = false - if !newPath.isEmpty && path.utf8.first != ._slash { + if !relativePath().isEmpty && path.utf8.first != ._slash { // Don't treat as first path segment when encoding path = "/" + path insertedSlash = true @@ -2220,13 +2219,30 @@ extension URL { pathToAppend = String(decoding: utf8, as: UTF8.self) } - if newPath.utf8.last != ._slash && pathToAppend.utf8.first != ._slash { - newPath += "/" - } else if newPath.utf8.last == ._slash && pathToAppend.utf8.first == ._slash { - _ = newPath.popLast() + func appendedPath() -> String { + var currentPath = relativePath() + if currentPath.isEmpty && !hasAuthority { + guard _parseInfo.scheme == nil else { + // Scheme only, append directly to the empty path, e.g. + // URL("scheme:").appending(path: "path") == scheme:path + return pathToAppend + } + // No scheme or authority, treat the empty path as "." + currentPath = "." + } + + // If currentPath is empty, pathToAppend is relative, and we have an authority, + // we must append a slash to separate the path from authority, which happens below. + + if currentPath.utf8.last != ._slash && pathToAppend.utf8.first != ._slash { + currentPath += "/" + } else if currentPath.utf8.last == ._slash && pathToAppend.utf8.first == ._slash { + _ = currentPath.popLast() + } + return currentPath + pathToAppend } - newPath += pathToAppend + var newPath = appendedPath() let hasTrailingSlash = newPath.utf8.last == ._slash let isDirectory: Bool diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index d00ef806d..437a18069 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -23,6 +23,18 @@ import TestSupport @testable import Foundation #endif +private func checkBehavior(_ result: T, new: T, old: T, file: StaticString = #filePath, line: UInt = #line) { + #if FOUNDATION_FRAMEWORK + if foundation_swift_url_enabled() { + XCTAssertEqual(result, new, file: file, line: line) + } else { + XCTAssertEqual(result, old, file: file, line: line) + } + #else + XCTAssertEqual(result, new, file: file, line: line) + #endif +} + final class URLTests : XCTestCase { func testURLBasics() throws { @@ -87,11 +99,7 @@ final class URLTests : XCTestCase { XCTAssertEqual(relativeURLWithBase.password(), baseURL.password()) XCTAssertEqual(relativeURLWithBase.host(), baseURL.host()) XCTAssertEqual(relativeURLWithBase.port, baseURL.port) - #if !FOUNDATION_FRAMEWORK_NSURL - XCTAssertEqual(relativeURLWithBase.path(), "/base/relative/path") - #else - XCTAssertEqual(relativeURLWithBase.path(), "relative/path") - #endif + checkBehavior(relativeURLWithBase.path(), new: "/base/relative/path", old: "relative/path") XCTAssertEqual(relativeURLWithBase.relativePath, "relative/path") XCTAssertEqual(relativeURLWithBase.query(), "query") XCTAssertEqual(relativeURLWithBase.fragment(), "fragment") @@ -154,7 +162,7 @@ final class URLTests : XCTestCase { "http:g" : "http:g", // For strict parsers ] - #if FOUNDATION_FRAMEWORK_NSURL + #if FOUNDATION_FRAMEWORK let testsFailingWithoutSwiftURL = Set([ "", "../../../g", @@ -165,8 +173,8 @@ final class URLTests : XCTestCase { #endif for test in tests { - #if FOUNDATION_FRAMEWORK_NSURL - if testsFailingWithoutSwiftURL.contains(test.key) { + #if FOUNDATION_FRAMEWORK + if !foundation_swift_url_enabled(), testsFailingWithoutSwiftURL.contains(test.key) { continue } #endif @@ -178,8 +186,8 @@ final class URLTests : XCTestCase { } func testURLPathAPIsResolveAgainstBase() throws { - #if FOUNDATION_FRAMEWORK_NSURL - try XCTSkipIf(true) + #if FOUNDATION_FRAMEWORK + try XCTSkipIf(!foundation_swift_url_enabled()) #endif // Borrowing the same test cases from RFC 3986, but checking paths let base = URL(string: "http://a/b/c/d;p?q") @@ -246,8 +254,8 @@ final class URLTests : XCTestCase { } func testURLPathComponentsPercentEncodedSlash() throws { - #if FOUNDATION_FRAMEWORK_NSURL - try XCTSkipIf(true) + #if FOUNDATION_FRAMEWORK + try XCTSkipIf(!foundation_swift_url_enabled()) #endif var url = try XCTUnwrap(URL(string: "https://example.com/https%3A%2F%2Fexample.com")) @@ -270,8 +278,8 @@ final class URLTests : XCTestCase { } func testURLRootlessPath() throws { - #if FOUNDATION_FRAMEWORK_NSURL - try XCTSkipIf(true) + #if FOUNDATION_FRAMEWORK + try XCTSkipIf(!foundation_swift_url_enabled()) #endif let paths = ["", "path"] @@ -565,13 +573,8 @@ final class URLTests : XCTestCase { // `appending(component:)` should explicitly treat `component` as a single // path component, meaning "/" should be encoded to "%2F" before appending appended = url.appending(component: slashComponent, directoryHint: .notDirectory) - #if FOUNDATION_FRAMEWORK_NSURL - XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/with:slash") - XCTAssertEqual(appended.relativePath, "relative/with:slash") - #else - XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/%2Fwith:slash") - XCTAssertEqual(appended.relativePath, "relative/%2Fwith:slash") - #endif + checkBehavior(appended.absoluteString, new: "file:///var/mobile/relative/%2Fwith:slash", old: "file:///var/mobile/relative/with:slash") + checkBehavior(appended.relativePath, new: "relative/%2Fwith:slash", old: "relative/with:slash") appended = url.appendingPathComponent(component, isDirectory: false) XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/no:slash") @@ -677,7 +680,48 @@ final class URLTests : XCTestCase { XCTAssertEqual(url.path(), "/path.foo/") url.append(path: "/////") url.deletePathExtension() - XCTAssertEqual(url.path(), "/path/") + // Old behavior only searches the last empty component, so the extension isn't actually removed + checkBehavior(url.path(), new: "/path/", old: "/path.foo///") + } + + func testURLAppendingToEmptyPath() throws { + let baseURL = URL(filePath: "/base/directory", directoryHint: .isDirectory) + let emptyPathURL = URL(filePath: "", relativeTo: baseURL) + let url = emptyPathURL.appending(path: "main.swift") + XCTAssertEqual(url.relativePath, "./main.swift") + XCTAssertEqual(url.path, "/base/directory/main.swift") + + var example = try XCTUnwrap(URL(string: "https://example.com")) + XCTAssertEqual(example.host(), "example.com") + XCTAssertTrue(example.path().isEmpty) + + // Appending to an empty path should add a slash if an authority exists + // The appended path should never become part of the host + example.append(path: "foo") + XCTAssertEqual(example.host(), "example.com") + XCTAssertEqual(example.path(), "/foo") + XCTAssertEqual(example.absoluteString, "https://example.com/foo") + + var emptyHost = try XCTUnwrap(URL(string: "scheme://")) + XCTAssertTrue(emptyHost.host()?.isEmpty ?? true) + XCTAssertTrue(emptyHost.path().isEmpty) + + emptyHost.append(path: "foo") + XCTAssertTrue(emptyHost.host()?.isEmpty ?? true) + // Old behavior failed to append correctly to an empty host + // Modern parsers agree that "foo" relative to "scheme://" is "scheme:///foo" + checkBehavior(emptyHost.path(), new: "/foo", old: "") + checkBehavior(emptyHost.absoluteString, new: "scheme:///foo", old: "scheme://") + + var schemeOnly = try XCTUnwrap(URL(string: "scheme:")) + XCTAssertTrue(schemeOnly.host()?.isEmpty ?? true) + XCTAssertTrue(schemeOnly.path().isEmpty) + + schemeOnly.append(path: "foo") + XCTAssertTrue(schemeOnly.host()?.isEmpty ?? true) + // Old behavior appends to the string, but is missing the path + checkBehavior(schemeOnly.path(), new: "foo", old: "") + XCTAssertEqual(schemeOnly.absoluteString, "scheme:foo") } func testURLComponentsPercentEncodedUnencodedProperties() throws { From 30f8171d34aa3486af1846aa01634a267b42236d Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:44:56 -0700 Subject: [PATCH 32/34] (128094957) String+Path parsing performance improvements (#927) (#1013) --- .../String/String+Path.swift | 665 +++++++++--------- .../DataIOTests.swift | 8 + .../StringTests.swift | 53 +- 3 files changed, 396 insertions(+), 330 deletions(-) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 1f86bb41a..3185c1fdb 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -27,7 +27,7 @@ import WASILibc internal import _FoundationCShims extension StringProtocol { - fileprivate func _standardizingSlashes() -> String { + fileprivate func _convertingSlashesIfNeeded() -> String { #if os(Windows) // The string functions below all assume that the path separator is a forward slash // Standardize the path to use forward slashes before processing for consistency @@ -43,132 +43,125 @@ extension StringProtocol { } extension String { + + // MARK: - Non-filesystem String Extensions + + internal var pathComponents: [String] { + _convertingSlashesIfNeeded()._pathComponents + } + + private var _pathComponents: [String] { + guard !isEmpty else { + return [] + } + var result = [String]() + + var start = startIndex + if utf8.first == ._slash { + result.append("/") + start = utf8.firstIndex { $0 != ._slash } ?? endIndex + } + var end = start + while end != endIndex { + end = utf8[end...].firstIndex(of: ._slash) ?? endIndex + if start != end { + result.append(String(self[start.. 1 && utf8.last == ._slash { + result.append("/") + } + return result + } + internal func deletingLastPathComponent() -> String { - _standardizingSlashes()._deletingLastPathComponent() + _convertingSlashesIfNeeded()._deletingLastPathComponent() } private func _deletingLastPathComponent() -> String { - let lastSlash = self.lastIndex { $0 == "/" } - guard let lastSlash else { - // No slash + guard let lastSlash = utf8.lastIndex(of: ._slash) else { + // No slash, entire string is deleted return "" } - - if lastSlash == startIndex { - // Only a first slash, return a bare slash. + + // Skip past consecutive slashes, if any (e.g. find "y" in "/my//path" or "h" in "/path//") + guard let lastNonSlash = self[.. String { - _standardizingSlashes()._appendingPathComponent(component) + _convertingSlashesIfNeeded()._appendingPathComponent(component) } private func _appendingPathComponent(_ component: String) -> String { - var result = self - if !component.isEmpty { - var needsSlash = true - if isEmpty { - needsSlash = false - } else if count == 1 { - needsSlash = first! != "/" - } else if count == 2 { - // "net" - needsSlash = !(self[startIndex] == "\\" && self[index(after: startIndex)] == "\\") - } - - if needsSlash { - result = result + "/" - } - - result = result + component + guard !isEmpty else { + return component._standardizingSlashes } - - result = result.reduce(into: "") { partialResult, c in - guard c == "/" else { - partialResult += String(c) - return - } - - guard !partialResult.isEmpty else { - partialResult += "/" - return - } - - let lastCharacter = partialResult.last! - if lastCharacter != "/" { - // Append the slash - partialResult += "/" - } - } - - if result.isEmpty { return "" } - - if result.last! != "/" { - return result - } - - var idx = result.endIndex - idx = result.index(before: idx) - while idx != result.startIndex && result[idx] == "/" { - idx = result.index(before: idx) + + if utf8.elementsEqual([._backslash, ._backslash]) { + return self + component._standardizingSlashes } - - return String(result[result.startIndex.. 1 else { + return self + } + + guard let lastSlash = utf8.lastIndex(of: ._slash) else { // No slash, just return self return self } - - if lastSlash == startIndex { - if count == 1 { - // Only a first slash, return a bare slash. - return "/" - } else { - return String(self[index(after: startIndex)..([ " ", "/", @@ -190,8 +183,7 @@ extension String { guard !pathExtension.isEmpty else { return self } - let dot = UInt8(ascii: ".") - guard let lastDot = self.utf8.lastIndex(of: dot) else { + guard let lastDot = utf8.lastIndex(of: ._dot) else { return self } var result = String(self[.. Bool { - guard pathExtension.utf8.last != UInt8(ascii: ".") else { + guard pathExtension.utf8.last != ._dot else { return false } - if let lastDot = pathExtension.utf8.lastIndex(of: UInt8(ascii: ".")) { - let beforeDot = pathExtension[.. String { - _standardizingSlashes()._merging(relativePath: relativePath) + _convertingSlashesIfNeeded()._merging(relativePath: relativePath) } private func _merging(relativePath: String) -> String { - guard relativePath.utf8.first != UInt8(ascii: "/") else { + guard relativePath.utf8.first != ._slash else { return relativePath } - guard let basePathEnd = self.utf8.lastIndex(of: UInt8(ascii: "/")) else { + guard let basePathEnd = self.utf8.lastIndex(of: ._slash) else { return relativePath } return self[...basePathEnd] + relativePath } internal var removingDotSegments: String { - _standardizingSlashes()._removingDotSegments + _convertingSlashesIfNeeded()._removingDotSegments } private var _removingDotSegments: String { - let input = self.utf8 - guard !input.isEmpty else { + guard !isEmpty else { return "" } - var output = [UInt8]() - enum DotState { + enum RemovingDotState { case initial case dot case dotDot @@ -280,108 +269,162 @@ extension String { case slashDotDot case appendUntilSlash } - let dot = UInt8(ascii: ".") - let slash = UInt8(ascii: "/") - var state = DotState.initial - for v in input { - switch state { - case .initial: - if v == dot { - state = .dot - } else if v == slash { - state = .slash - } else { - output.append(v) - state = .appendUntilSlash - } - break - case .dot: - if v == dot { - state = .dotDot - } else if v == slash { - state = .initial - } else { - output.append(contentsOf: [dot, v]) - state = .appendUntilSlash - } - break - case .dotDot: - if v == slash { - state = .initial - } else { - output.append(contentsOf: [dot, dot, v]) - state = .appendUntilSlash - } - break - case .slash: - if v == dot { - state = .slashDot - } else if v == slash { - output.append(slash) - } else { - output.append(contentsOf: [slash, v]) - state = .appendUntilSlash + return String(unsafeUninitializedCapacity: utf8.count) { buffer in + + // State machine for remove_dot_segments() from RFC 3986 + // + // First, remove all "./" and "../" prefixes by moving through + // the .initial, .dot, and .dotDot states (without appending). + // + // Then, move through the remaining states/components, first + // checking if the component is special ("/./" or "/../") so + // that we only append when necessary. + + var state = RemovingDotState.initial + var i = 0 + for v in utf8 { + switch state { + case .initial: + if v == ._dot { + state = .dot + } else if v == ._slash { + state = .slash + } else { + buffer[i] = v + i += 1 + state = .appendUntilSlash + } + case .dot: + if v == ._dot { + state = .dotDot + } else if v == ._slash { + state = .initial + } else { + i = buffer[i...i+1].initialize(fromContentsOf: [._dot, v]) + state = .appendUntilSlash + } + case .dotDot: + if v == ._slash { + state = .initial + } else { + i = buffer[i...i+2].initialize(fromContentsOf: [._dot, ._dot, v]) + state = .appendUntilSlash + } + case .slash: + if v == ._dot { + state = .slashDot + } else if v == ._slash { + buffer[i] = ._slash + i += 1 + } else { + i = buffer[i...i+1].initialize(fromContentsOf: [._slash, v]) + state = .appendUntilSlash + } + case .slashDot: + if v == ._dot { + state = .slashDotDot + } else if v == ._slash { + state = .slash + } else { + i = buffer[i...i+2].initialize(fromContentsOf: [._slash, ._dot, v]) + state = .appendUntilSlash + } + case .slashDotDot: + if v == ._slash { + // Cheaply remove the previous component by moving i to its start + i = buffer[.. String { + guard utf8.count > 1 else { + return self } - output.append(0) // NULL-terminated + enum SlashState { + case initial + case slash + } + + return String(unsafeUninitializedCapacity: utf8.count) { buffer in + var state = SlashState.initial + var i = 0 + for v in utf8 { + switch state { + case .initial: + buffer[i] = v + i += 1 + if v == ._slash { + state = .slash + } + case .slash: + if v != ._slash { + buffer[i] = v + i += 1 + state = .initial + } + } + } + return i + } + } + + internal var _droppingTrailingSlashes: String { + guard !self.isEmpty else { + return self + } + guard let lastNonSlash = utf8.lastIndex(where: { $0 != ._slash }) else { + // It's all /'s so just return a single slash + return "/" + } + return String(self[...lastNonSlash]) + } - return String(cString: output) + private var _standardizingSlashes: String { + _compressingSlashes()._droppingTrailingSlashes } +// MARK: - Filesystem String Extensions + #if !NO_FILESYSTEM + internal static func homeDirectoryPath(forUser user: String? = nil) -> String { -#if os(Windows) + #if os(Windows) if let user { func fallbackUserDirectory() -> String { guard let fallback = ProcessInfo.processInfo.environment["ALLUSERSPROFILE"] else { @@ -455,7 +498,8 @@ extension String { } return String(decodingCString: $0.baseAddress!, as: UTF16.self) } -#else + #else // os(Windows) + #if targetEnvironment(simulator) if user == nil, let envValue = getenv("CFFIXED_USER_HOME") ?? getenv("HOME") { return String(cString: envValue).standardizingPath @@ -480,8 +524,8 @@ extension String { return dir.standardizingPath } } - #endif - + #endif // !os(WASI) + // Fallback to HOME for the current user if possible if user == nil, let home = getenv("HOME") { return String(cString: home).standardizingPath @@ -489,27 +533,28 @@ extension String { // If all else fails, log and fall back to /var/empty return "/var/empty" -#endif + #endif // os(Windows) } // From swift-corelibs-foundation's NSTemporaryDirectory. Internal for now, pending a better public API. internal static var temporaryDirectoryPath: String { func normalizedPath(with path: String) -> String { - var result = path._standardizingSlashes() + let result = path._convertingSlashesIfNeeded() guard result.utf8.last != ._slash else { return result } return result + "/" } -#if os(Windows) + #if os(Windows) let cchLength: DWORD = GetTempPathW(0, nil) var wszPath: [WCHAR] = Array(repeating: 0, count: Int(cchLength + 1)) guard GetTempPathW(DWORD(wszPath.count), &wszPath) <= cchLength else { preconditionFailure("GetTempPathW mutation race") } return normalizedPath(with: String(decodingCString: wszPath, as: UTF16.self).standardizingPath) -#else -#if canImport(Darwin) + #else // os(Windows) + + #if canImport(Darwin) // If confstr returns 0 it either failed or the variable had no content // If the variable had no content, we should continue on below // If it fails, we should also silently ignore the error and continue on below. This API can fail for non-programmer reasons such as the device being out of storage space when libSystem attempts to create the directory @@ -528,80 +573,57 @@ extension String { return result } } -#endif -#if !os(WASI) + #endif // canImport(Darwin) + + #if !os(WASI) if let envValue = Platform.getEnvSecure("TMPDIR") { return normalizedPath(with: envValue) } -#endif -#if os(Android) + #endif + + #if os(Android) // Bionic uses /data/local/tmp/ as temporary directory. TMPDIR is rarely // defined. return "/data/local/tmp/" -#else + #else return "/tmp/" -#endif -#endif // os(Windows) - } -#endif // !NO_FILESYSTEM - - /// Replaces any number of sequential `/` - /// characters with / - /// NOTE: Internal so it's testable - /// - Returns: The replaced String - internal func _transmutingCompressingSlashes() -> String { - let input = self.utf8 - guard input.count > 1 else { - return self - } - - enum SlashState { - case initial - case slash - } - - return String(unsafeUninitializedCapacity: input.count) { buffer in - var state = SlashState.initial - var i = 0 - for v in input { - switch state { - case .initial: - buffer[i] = v - i += 1 - if v == ._slash { - state = .slash - } - case .slash: - if v != ._slash { - buffer[i] = v - i += 1 - state = .initial - } - } - } - return i - } - } - - internal var _droppingTrailingSlashes: String { - guard !self.isEmpty else { - return self - } - guard let lastNonSlash = self.lastIndex(where: { $0 != "/"}) else { - // It's all /'s so just return a single slash - return "/" - } - return String(self[...lastNonSlash]) - } - -#if !NO_FILESYSTEM + #endif + #endif // os(Windows) + } + + private static let NETWORK_PREFIX: [UInt8] = [._backslash, ._backslash] + + private static let _automountPrefixes = { + let prefixes: [[UInt8]] = [ + Array("/private/var/automount/".utf8), + Array("/var/automount/".utf8), + Array("/private/".utf8) + ] + return prefixes + }() + + private static let _automountExclusionList = { + let exclusions: [[UInt8]] = [ + Array("/Applications".utf8), + Array("/Library".utf8), + Array("/System".utf8), + Array("/Users".utf8), + Array("/Volumes".utf8), + Array("/bin".utf8), + Array("/cores".utf8), + Array("/dev".utf8), + Array("/opt".utf8), + Array("/private".utf8), + Array("/sbin".utf8), + Array("/usr".utf8) + ] + return exclusions + }() - static var NETWORK_PREFIX: String { #"\\"# } - private var _standardizingPath: String { - var result = _standardizingSlashes()._transmutingCompressingSlashes()._droppingTrailingSlashes - let postNetStart = if result.starts(with: String.NETWORK_PREFIX) { - result.firstIndex(of: "/") ?? result.endIndex + var result = _convertingSlashesIfNeeded()._standardizingSlashes + let postNetStart = if result.utf8.starts(with: String.NETWORK_PREFIX) { + result.utf8.firstIndex(of: ._slash) ?? result.endIndex } else { result.startIndex } @@ -613,15 +635,15 @@ extension String { result = result._removingDotSegments // Automounted paths need to be stripped for various flavors of paths - let exclusionList = ["/Applications", "/Library", "/System", "/Users", "/Volumes", "/bin", "/cores", "/dev", "/opt", "/private", "/sbin", "/usr"] - for path in ["/private/var/automount", "/var/automount", "/private"] { - if result.starts(with: "\(path)/") { - let newPath = String(result.dropFirst(path.count)) - let isExcluded = exclusionList.contains { - newPath == $0 || newPath.starts(with: "\($0)/") + for prefix in String._automountPrefixes { + if result.utf8.starts(with: prefix) { + let prefixEndSlash = result.utf8.index(result.startIndex, offsetBy: prefix.count - 1) + let newPath = result[prefixEndSlash...] + let isExcluded = String._automountExclusionList.contains { + newPath._hasPathPrefix($0) } - if !isExcluded && FileManager.default.fileExists(atPath: newPath) { - result = newPath + if !isExcluded && FileManager.default.fileExists(atPath: String(newPath)) { + result = String(newPath) } break } @@ -633,47 +655,28 @@ extension String { expandingTildeInPath._standardizingPath } -#endif // !NO_FILESYSTEM - - // _NSPathComponents - var pathComponents: [String] { - _standardizingSlashes()._pathComponents - } - - private var _pathComponents: [String] { - var components = self.components(separatedBy: "/").filter { !$0.isEmpty } - if self.first == "/" { - components.insert("/", at: 0) - } - if self.last == "/" && self.count > 1 { - components.append("/") - } - return components - } - - #if !NO_FILESYSTEM var abbreviatingWithTildeInPath: String { - _standardizingSlashes()._abbreviatingWithTildeInPath + _convertingSlashesIfNeeded()._abbreviatingWithTildeInPath } private var _abbreviatingWithTildeInPath: String { guard !self.isEmpty && self != "/" else { return self } - let homeDir = String.homeDirectoryPath() - guard self.starts(with: homeDir) else { return self } - let nextIdxInOriginal = self.unicodeScalars.index(self.startIndex, offsetBy: homeDir.unicodeScalars.count) - guard nextIdxInOriginal == self.endIndex || self[nextIdxInOriginal] == "/" else { return self } + let homeDir = String.homeDirectoryPath().utf8 + guard utf8.starts(with: homeDir) else { return self } + let nextIdxInOriginal = utf8.index(startIndex, offsetBy: homeDir.count) + guard nextIdxInOriginal == endIndex || utf8[nextIdxInOriginal] == ._slash else { return self } return "~" + self[nextIdxInOriginal...] } var expandingTildeInPath: String { - _standardizingSlashes()._expandingTildeInPath + _convertingSlashesIfNeeded()._expandingTildeInPath } private var _expandingTildeInPath: String { - guard self.first == "~" else { return self } + guard utf8.first == UInt8(ascii: "~") else { return self } var user: String? = nil - let firstSlash = self.firstIndex(of: "/") ?? self.endIndex - let indexAfterTilde = self.index(after: self.startIndex) + let firstSlash = utf8.firstIndex(of: ._slash) ?? endIndex + let indexAfterTilde = utf8.index(after: startIndex) if firstSlash != indexAfterTilde { user = String(self[indexAfterTilde ..< firstSlash]) } @@ -699,13 +702,12 @@ extension String { } #else return nil - #endif + #endif // canImport(Darwin) } func _resolvingSymlinksInPath() -> String? { guard !isEmpty else { return nil } - -#if os(Windows) + #if os(Windows) return try? self.withNTPathRepresentation { let hFile: HANDLE = CreateFileW($0, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nil) if hFile == INVALID_HANDLE_VALUE { @@ -723,7 +725,7 @@ extension String { return String(decodingCString: $0.baseAddress!.advanced(by: 4), as: UTF16.self) } } -#else + #else // os(Windows) return self.withFileSystemRepresentation { fsPtr -> String? in guard let fsPtr else { return nil } // If not using the cache (which may not require hitting the disk at all if it's warm), try getting the full path from getattrlist. @@ -808,7 +810,7 @@ extension String { } } } -#endif + #endif // os(Windows) } var resolvingSymlinksInPath: String { @@ -818,15 +820,20 @@ extension String { } return result._standardizingPath } - #endif // !NO_FILESYSTEM + +#endif // !NO_FILESYSTEM + } -fileprivate enum DotState { +// MARK: - StringProtocol Helper Extensions + +fileprivate enum DotDotState { case initial case dot case dotDot case lookingForSlash } + extension StringProtocol { internal func replacing(_ a: UInt8, with b: UInt8) -> String { var utf8Array = Array(self.utf8) @@ -848,13 +855,12 @@ extension StringProtocol { // Internal for testing purposes internal func _hasDotDotComponent() -> Bool { - let input = self.utf8 - guard input.count >= 2 else { + guard utf8.count >= 2 else { return false } - var state = DotState.initial - for v in input { + var state = DotDotState.initial + for v in utf8 { switch state { case .initial: if v == ._dot { @@ -874,7 +880,7 @@ extension StringProtocol { } case .dotDot: if v == ._slash { - return true // Starts with "../" + return true // Starts with "../" or contains "/../" } else { state = .lookingForSlash } @@ -886,6 +892,15 @@ extension StringProtocol { } } } - return state == .dotDot + return state == .dotDot // Is ".." or ends with "/.." + } + + // Returns true if self == prefix or self starts with (prefix + "/") + internal func _hasPathPrefix(_ prefix: some Collection) -> Bool { + guard utf8.starts(with: prefix) else { + return false + } + let prefixEnd = utf8.index(startIndex, offsetBy: prefix.count) + return prefixEnd == endIndex || utf8[prefixEnd] == ._slash } } diff --git a/Tests/FoundationEssentialsTests/DataIOTests.swift b/Tests/FoundationEssentialsTests/DataIOTests.swift index 6b9ecf3f4..cfe212d62 100644 --- a/Tests/FoundationEssentialsTests/DataIOTests.swift +++ b/Tests/FoundationEssentialsTests/DataIOTests.swift @@ -258,6 +258,12 @@ class DataIOTests : XCTestCase { XCTAssertEqual("/a/b/c/".deletingLastPathComponent(), "/a/b") XCTAssertEqual("hello".deletingLastPathComponent(), "") XCTAssertEqual("hello/".deletingLastPathComponent(), "") + XCTAssertEqual("/hello/".deletingLastPathComponent(), "/") + XCTAssertEqual("hello///".deletingLastPathComponent(), "") + XCTAssertEqual("a/".deletingLastPathComponent(), "") + XCTAssertEqual("a/b".deletingLastPathComponent(), "a") + XCTAssertEqual("a/b/".deletingLastPathComponent(), "a") + XCTAssertEqual("a//b//".deletingLastPathComponent(), "a") } func testAppendingPathComponent() { @@ -292,6 +298,8 @@ class DataIOTests : XCTestCase { XCTAssertEqual("/a/b/c/".lastPathComponent, "c") XCTAssertEqual("hello".lastPathComponent, "hello") XCTAssertEqual("hello/".lastPathComponent, "hello") + XCTAssertEqual("hello///".lastPathComponent, "hello") + XCTAssertEqual("//a//".lastPathComponent, "a") } } diff --git a/Tests/FoundationEssentialsTests/StringTests.swift b/Tests/FoundationEssentialsTests/StringTests.swift index ec412cce8..b0e936a8d 100644 --- a/Tests/FoundationEssentialsTests/StringTests.swift +++ b/Tests/FoundationEssentialsTests/StringTests.swift @@ -416,6 +416,7 @@ final class StringTests : XCTestCase { XCTAssertEqual("//".lastPathComponent, "/") XCTAssertEqual("/////".lastPathComponent, "/") XCTAssertEqual("/./..//./..//".lastPathComponent, "..") + XCTAssertEqual("/😎/😂/❤️/".lastPathComponent, "❤️") } func testRemovingDotSegments() { @@ -858,6 +859,34 @@ final class StringTests : XCTestCase { XCTAssertEqual("/path.foo//".deletingPathExtension(), "/path/") } + func testPathComponents() { + let tests: [(String, [String])] = [ + ("", []), + ("/", ["/"]), + ("//", ["/", "/"]), + ("a", ["a"]), + ("/a", ["/", "a"]), + ("a/", ["a", "/"]), + ("/a/", ["/", "a", "/"]), + ("///", ["/", "/"]), + ("//a", ["/", "a"]), + ("a//", ["a", "/"]), + ("//a//", ["/", "a", "/"]), + ("a/b/c", ["a", "b", "c"]), + ("/a/b/c", ["/", "a", "b", "c"]), + ("a/b/c/", ["a", "b", "c", "/"]), + ("/a/b/c/", ["/", "a", "b", "c", "/"]), + ("/abc//def///ghi/jkl//123///456/7890//", ["/", "abc", "def", "ghi", "jkl", "123", "456", "7890", "/"]), + ("/😎/😂/❤️/", ["/", "😎", "😂", "❤️", "/"]), + ("J'aime//le//café//☕️", ["J'aime", "le", "café", "☕️"]), + ("U+2215∕instead∕of∕slash(U+002F)", ["U+2215∕instead∕of∕slash(U+002F)"]), + ] + for (input, expected) in tests { + let result = input.pathComponents + XCTAssertEqual(result, expected) + } + } + func test_dataUsingEncoding() { let s = "hello 🧮" @@ -1006,33 +1035,47 @@ final class StringTests : XCTestCase { XCTAssertEqual("e\u{301}\u{301}f".data(using: .nonLossyASCII, allowLossyConversion: true), Data([UInt8(ascii: "e"), UInt8(ascii: "?"), UInt8(ascii: "?"), UInt8(ascii: "f")])) } - func test_transmutingCompressingSlashes() { + func test_compressingSlashes() { let testCases: [(String, String)] = [ + ("", ""), // Empty string + ("/", "/"), // Single slash ("/////", "/"), // All slashes ("ABCDE", "ABCDE"), // No slashes ("//ABC", "/ABC"), // Starts with multiple slashes ("/ABCD", "/ABCD"), // Starts with single slash ("ABC//", "ABC/"), // Ends with multiple slashes ("ABCD/", "ABCD/"), // Ends with single slash - ("AB//DF/GH//I", "AB/DF/GH/I") // Internal slashes + ("//ABC//", "/ABC/"), // Starts and ends with multiple slashes + ("AB/CD", "AB/CD"), // Single internal slash + ("AB//DF/GH//I", "AB/DF/GH/I"), // Internal slashes + ("//😎///😂/❤️//", "/😎/😂/❤️/") ] for (testString, expectedResult) in testCases { let result = testString - ._transmutingCompressingSlashes() + ._compressingSlashes() XCTAssertEqual(result, expectedResult) } } func test_pathHasDotDotComponent() { let testCases: [(String, Bool)] = [ - ("../AB", true), //Begins with .. + ("../AB", true), // Begins with .. ("/ABC/..", true), // Ends with .. ("/ABC/../DEF", true), // Internal .. ("/ABC/DEF..", false), // Ends with .. but not part of path ("ABC/../../DEF", true), // Multiple internal dot dot ("/AB/./CD", false), // Internal single dot ("/AB/..../CD", false), // Internal multiple dots - ("..", true) // Dot dot only + ("..", true), // Dot dot only + ("...", false), + ("..AB", false), + ("..AB/", false), + ("..AB/..", true), + (".AB/./.", false), + ("/..AB/", false), + ("A../", false), + ("/..", true), + ("././/./.", false) ] for (testString, expectedResult) in testCases { let result = testString From a28f6ac7af415d0061ae3a73644055893f8f15c0 Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:44:30 -0700 Subject: [PATCH 33/34] (138059777) URL.deletingLastPathComponent() should append .. in special cases (#989) (#1022) --- Sources/FoundationEssentials/URL/URL.swift | 34 +++- .../FoundationEssentialsTests/URLTests.swift | 150 ++++++++++++++++++ 2 files changed, 179 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index 063d5f9f3..e68facd5e 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -1351,6 +1351,11 @@ public struct URL: Equatable, Sendable, Hashable { return URL.fileSystemPath(for: path()) } + /// True if the URL's relative path would resolve against a base URL path + private var pathResolvesAgainstBase: Bool { + return _parseInfo.scheme == nil && !hasAuthority && relativePath().utf8.first != ._slash + } + /// Returns the path component of the URL if present, otherwise returns an empty string. /// /// - note: This function will resolve against the base `URL`. @@ -1643,7 +1648,9 @@ public struct URL: Equatable, Sendable, Hashable { /// Returns a URL constructed by removing the last path component of self. /// /// This function may either remove a path component or append `/..`. - /// If the URL has an empty path (e.g., `http://www.example.com`), then this function will return the URL unchanged. + /// If the URL has an empty path that is not resolved against a base URL + /// (e.g., `http://www.example.com`), + /// then this function will return the URL unchanged. public func deletingLastPathComponent() -> URL { #if FOUNDATION_FRAMEWORK guard foundation_swift_url_enabled() else { @@ -1652,13 +1659,30 @@ public struct URL: Equatable, Sendable, Hashable { return result } #endif - guard !relativePath().isEmpty else { return self } - var components = URLComponents(parseInfo: _parseInfo) - var newPath = components.percentEncodedPath.deletingLastPathComponent() + let path = relativePath() + let shouldAppendDotDot = ( + pathResolvesAgainstBase && ( + path.isEmpty + || path.lastPathComponent == "." + || path.lastPathComponent == ".." + ) + ) + + var newPath = path + if newPath.lastPathComponent != ".." { + newPath = newPath.deletingLastPathComponent() + } + if shouldAppendDotDot { + newPath = newPath.appendingPathComponent("..") + } + if newPath.isEmpty && pathResolvesAgainstBase { + newPath = "." + } // .deletingLastPathComponent() removes the trailing "/", but we know it's a directory - if !newPath.isEmpty, newPath.utf8.last != UInt8(ascii: "/") { + if !newPath.isEmpty && newPath.utf8.last != ._slash { newPath += "/" } + var components = URLComponents(parseInfo: _parseInfo) components.percentEncodedPath = newPath return components.url(relativeTo: baseURL)! } diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 437a18069..7feaddc4e 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -586,6 +586,156 @@ final class URLTests : XCTestCase { XCTAssertEqual(appended.relativePath, "relative/with:slash") } + func testURLDeletingLastPathComponent() throws { + var absolute = URL(filePath: "/absolute/path", directoryHint: .notDirectory) + // Note: .relativePath strips the trailing slash for compatibility + XCTAssertEqual(absolute.relativePath, "/absolute/path") + XCTAssertFalse(absolute.hasDirectoryPath) + + absolute.deleteLastPathComponent() + XCTAssertEqual(absolute.relativePath, "/absolute") + XCTAssertTrue(absolute.hasDirectoryPath) + + absolute.deleteLastPathComponent() + XCTAssertEqual(absolute.relativePath, "/") + XCTAssertTrue(absolute.hasDirectoryPath) + + // The old .deleteLastPathComponent() implementation appends ".." to the + // root directory "/", resulting in "/../". This resolves back to "/". + // The new implementation simply leaves "/" as-is. + absolute.deleteLastPathComponent() + checkBehavior(absolute.relativePath, new: "/", old: "/..") + XCTAssertTrue(absolute.hasDirectoryPath) + + absolute.append(path: "absolute", directoryHint: .isDirectory) + checkBehavior(absolute.path, new: "/absolute", old: "/../absolute") + + // Reset `var absolute` to "/absolute" to prevent having + // a "/../" prefix in all the old expectations. + absolute = URL(filePath: "/absolute", directoryHint: .isDirectory) + + var relative = URL(filePath: "relative/path", directoryHint: .notDirectory, relativeTo: absolute) + XCTAssertEqual(relative.relativePath, "relative/path") + XCTAssertFalse(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/absolute/relative/path") + + relative.deleteLastPathComponent() + XCTAssertEqual(relative.relativePath, "relative") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/absolute/relative") + + relative.deleteLastPathComponent() + XCTAssertEqual(relative.relativePath, ".") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/absolute") + + relative.deleteLastPathComponent() + XCTAssertEqual(relative.relativePath, "..") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/") + + relative.deleteLastPathComponent() + XCTAssertEqual(relative.relativePath, "../..") + XCTAssertTrue(relative.hasDirectoryPath) + checkBehavior(relative.path, new:"/", old: "/..") + + relative.append(path: "path", directoryHint: .isDirectory) + XCTAssertEqual(relative.relativePath, "../../path") + XCTAssertTrue(relative.hasDirectoryPath) + checkBehavior(relative.path, new: "/path", old: "/../path") + + relative.deleteLastPathComponent() + XCTAssertEqual(relative.relativePath, "../..") + XCTAssertTrue(relative.hasDirectoryPath) + checkBehavior(relative.path, new: "/", old: "/..") + + relative = URL(filePath: "", relativeTo: absolute) + checkBehavior(relative.relativePath, new: "", old: ".") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/absolute") + + relative.deleteLastPathComponent() + XCTAssertEqual(relative.relativePath, "..") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/") + + relative.deleteLastPathComponent() + XCTAssertEqual(relative.relativePath, "../..") + XCTAssertTrue(relative.hasDirectoryPath) + checkBehavior(relative.path, new: "/", old: "/..") + + relative = URL(filePath: "relative/./", relativeTo: absolute) + // According to RFC 3986, "." and ".." segments should not be removed + // until the path is resolved against the base URL (when calling .path) + checkBehavior(relative.relativePath, new: "relative/.", old: "relative") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/absolute/relative") + + relative.deleteLastPathComponent() + checkBehavior(relative.relativePath, new: "relative/..", old: ".") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/absolute") + + relative = URL(filePath: "relative/.", directoryHint: .isDirectory, relativeTo: absolute) + checkBehavior(relative.relativePath, new: "relative/.", old: "relative") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/absolute/relative") + + relative.deleteLastPathComponent() + checkBehavior(relative.relativePath, new: "relative/..", old: ".") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/absolute") + + relative = URL(filePath: "relative/..", relativeTo: absolute) + XCTAssertEqual(relative.relativePath, "relative/..") + checkBehavior(relative.hasDirectoryPath, new: true, old: false) + XCTAssertEqual(relative.path, "/absolute") + + relative.deleteLastPathComponent() + XCTAssertEqual(relative.relativePath, "relative/../..") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/") + + relative = URL(filePath: "relative/..", directoryHint: .isDirectory, relativeTo: absolute) + XCTAssertEqual(relative.relativePath, "relative/..") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/absolute") + + relative.deleteLastPathComponent() + XCTAssertEqual(relative.relativePath, "relative/../..") + XCTAssertTrue(relative.hasDirectoryPath) + XCTAssertEqual(relative.path, "/") + + var url = try XCTUnwrap(URL(string: "scheme://host.with.no.path")) + XCTAssertTrue(url.path().isEmpty) + + url.deleteLastPathComponent() + XCTAssertEqual(url.absoluteString, "scheme://host.with.no.path") + XCTAssertTrue(url.path().isEmpty) + + let unusedBase = URL(string: "base://url") + url = try XCTUnwrap(URL(string: "scheme://host.with.no.path", relativeTo: unusedBase)) + XCTAssertEqual(url.absoluteString, "scheme://host.with.no.path") + XCTAssertTrue(url.path().isEmpty) + + url.deleteLastPathComponent() + XCTAssertEqual(url.absoluteString, "scheme://host.with.no.path") + XCTAssertTrue(url.path().isEmpty) + + var schemeRelative = try XCTUnwrap(URL(string: "scheme:relative/path")) + // Bug in the old implementation where a relative path is not recognized + checkBehavior(schemeRelative.relativePath, new: "relative/path", old: "") + + schemeRelative.deleteLastPathComponent() + checkBehavior(schemeRelative.relativePath, new: "relative", old: "") + + schemeRelative.deleteLastPathComponent() + XCTAssertEqual(schemeRelative.relativePath, "") + + schemeRelative.deleteLastPathComponent() + XCTAssertEqual(schemeRelative.relativePath, "") + } + func testURLFilePathDropsTrailingSlashes() throws { var url = URL(filePath: "/path/slashes///") XCTAssertEqual(url.path(), "/path/slashes///") From 26f4864e64eb56010d7438089e4373dfc3adfb25 Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:45:06 -0700 Subject: [PATCH 34/34] (138168197) Restore URL.host bracket stripping for compatibility (#1008) (#1023) --- Sources/FoundationEssentials/URL/URL.swift | 41 +++++++++++++------ .../FoundationEssentialsTests/URLTests.swift | 36 ++++++++++++++++ 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index e68facd5e..9e197fd48 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -1207,21 +1207,38 @@ public struct URL: Equatable, Sendable, Hashable { return nil } #endif - guard let encodedHost else { return nil } - let didPercentEncodeHost = hasAuthority ? _parseInfo.didPercentEncodeHost : _baseParseInfo?.didPercentEncodeHost ?? false - if percentEncoded { - if didPercentEncodeHost { - return String(encodedHost) - } - guard let decoded = Parser.IDNADecodeHost(encodedHost) else { + guard let encodedHost else { + return nil + } + + func requestedHost() -> String? { + let didPercentEncodeHost = hasAuthority ? _parseInfo.didPercentEncodeHost : _baseParseInfo?.didPercentEncodeHost ?? false + if percentEncoded { + if didPercentEncodeHost { + return encodedHost + } + guard let decoded = Parser.IDNADecodeHost(encodedHost) else { + return encodedHost + } + return Parser.percentEncode(decoded, component: .host) + } else { + if didPercentEncodeHost { + return Parser.percentDecode(encodedHost) + } return encodedHost } - return Parser.percentEncode(decoded, component: .host) + } + + guard let requestedHost = requestedHost() else { + return nil + } + + let isIPLiteral = hasAuthority ? _parseInfo.isIPLiteral : _baseParseInfo?.isIPLiteral ?? false + if isIPLiteral { + // Strip square brackets to be compatible with old URL.host behavior + return String(requestedHost.utf8.dropFirst().dropLast()) } else { - if didPercentEncodeHost { - return Parser.percentDecode(encodedHost) - } - return String(encodedHost) + return requestedHost } } diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 7feaddc4e..0998dd586 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -792,6 +792,42 @@ final class URLTests : XCTestCase { XCTAssertEqual(url.host, "*.xn--poema-9qae5a.com.br") } + func testURLHostIPLiteralCompatibility() throws { + var url = URL(string: "http://[::]")! + XCTAssertEqual(url.host, "::") + XCTAssertEqual(url.host(), "::") + + url = URL(string: "https://[::1]:433/")! + XCTAssertEqual(url.host, "::1") + XCTAssertEqual(url.host(), "::1") + + url = URL(string: "https://[2001:db8::]/")! + XCTAssertEqual(url.host, "2001:db8::") + XCTAssertEqual(url.host(), "2001:db8::") + + url = URL(string: "https://[2001:db8::]:433")! + XCTAssertEqual(url.host, "2001:db8::") + XCTAssertEqual(url.host(), "2001:db8::") + + url = URL(string: "http://[fe80::a%25en1]")! + XCTAssertEqual(url.absoluteString, "http://[fe80::a%25en1]") + XCTAssertEqual(url.host, "fe80::a%en1") + XCTAssertEqual(url.host(percentEncoded: true), "fe80::a%25en1") + XCTAssertEqual(url.host(percentEncoded: false), "fe80::a%en1") + + url = URL(string: "http://[fe80::a%en1]")! + XCTAssertEqual(url.absoluteString, "http://[fe80::a%25en1]") + XCTAssertEqual(url.host, "fe80::a%en1") + XCTAssertEqual(url.host(percentEncoded: true), "fe80::a%25en1") + XCTAssertEqual(url.host(percentEncoded: false), "fe80::a%en1") + + url = URL(string: "http://[fe80::a%100%CustomZone]")! + XCTAssertEqual(url.absoluteString, "http://[fe80::a%25100%25CustomZone]") + XCTAssertEqual(url.host, "fe80::a%100%CustomZone") + XCTAssertEqual(url.host(percentEncoded: true), "fe80::a%25100%25CustomZone") + XCTAssertEqual(url.host(percentEncoded: false), "fe80::a%100%CustomZone") + } + func testURLTildeFilePath() throws { var url = URL(filePath: "~") // "~" must either be expanded to an absolute path or resolved against a base URL