diff --git a/CMakeLists.txt b/CMakeLists.txt index b99fcfa8f..a8dc410cf 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) @@ -45,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/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/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/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. 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/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/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 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..7b35f1189 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 @@ -945,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/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..991c5e817 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 @@ -83,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 } @@ -127,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 { @@ -158,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/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 f8a770026..b8cd50a4c 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 @@ -120,8 +123,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 @@ -373,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 @@ -555,7 +565,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 +577,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 +588,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/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..03adcc6fa 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 @@ -805,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 { @@ -814,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/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/JSON/JSONDecoder.swift b/Sources/FoundationEssentials/JSON/JSONDecoder.swift index 2b709d0ed..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 } } @@ -1128,6 +1129,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 +1154,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 +1285,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 +1310,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 +1477,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 +1502,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/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 20dc561af..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) @@ -49,8 +52,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..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 { @@ -581,13 +583,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) 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 79bcbfa76..477d5d39b 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 @@ -365,33 +367,32 @@ 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(decoding: $0, 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 { + 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 { - fatalError("unable to lookup SID for user \(user)") + LookupAccountNameW(nil, pwszUserName, nil, &cbSID, nil, &cchReferencedDomainName, &eUse) + guard cbSID > 0 else { + 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 @@ -399,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 @@ -421,7 +423,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 @@ -436,7 +438,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/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/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index d23855889..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`. @@ -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) ?? "" } @@ -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/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/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 diff --git a/Sources/FoundationMacros/CMakeLists.txt b/Sources/FoundationMacros/CMakeLists.txt index 0cc31c299..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 @@ -44,16 +60,18 @@ target_link_libraries(FoundationMacros PUBLIC ) set_target_properties(FoundationMacros PROPERTIES - INSTALL_RPATH "$ORIGIN" - INSTALL_REMOVE_ENVIRONMENT_RPATH ON) + 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 -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>") + 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) install(TARGETS FoundationMacros - ARCHIVE DESTINATION lib/swift/host/plugins LIBRARY DESTINATION lib/swift/host/plugins - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + RUNTIME DESTINATION bin) 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) 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 - } 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 84f180a83..a25638ab4 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 { @@ -526,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 @@ -546,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")) } } @@ -864,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 { @@ -890,13 +952,26 @@ 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 } } + + 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 + } } diff --git a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift index 4fd275d7b..20e29f345 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 { @@ -1359,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) { diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 6366f1d2e..ca2dec76c 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")! @@ -554,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() @@ -1014,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") + } } 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")