diff --git a/CMakeLists.txt b/CMakeLists.txt index b99fcfa8f..3243e5398 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) @@ -104,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/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..67bce500a 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") @@ -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/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..5525efec6 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) @@ -66,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 99d72e161..257b742ec 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar.swift @@ -16,8 +16,12 @@ internal import os import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +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 fd863cef8..8c25c77f6 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift @@ -16,8 +16,12 @@ internal import os import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +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 eb66e3f7c..612681d2d 100644 --- a/Sources/FoundationEssentials/Data/Data+Reading.swift +++ b/Sources/FoundationEssentials/Data/Data+Reading.swift @@ -22,15 +22,19 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +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 { #if canImport(Darwin) return fgetxattr(fd, name, value, size, position, options) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) || canImport(Android) return fgetxattr(fd, name, value, size) #else return -1 @@ -351,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/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 4f92cb1a6..0256e51ef 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -24,9 +24,13 @@ import Android import unistd #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif #if !NO_FILESYSTEM @@ -127,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("/") @@ -181,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)` @@ -514,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 @@ -537,6 +547,7 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint fchmod(fd, mode) #endif } +#endif // os(WASI) } } } @@ -623,7 +634,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..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") @@ -2077,6 +2079,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 +2106,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..37548e498 100644 --- a/Sources/FoundationEssentials/Date.swift +++ b/Sources/FoundationEssentials/Date.swift @@ -16,8 +16,12 @@ import Darwin import Bionic #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +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 172454463..eb344b214 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift @@ -16,8 +16,12 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(CRT) import CRT +#elseif os(WASI) +import WASILibc #endif private let powerOfTen: [Decimal.VariableLengthInteger] = [ @@ -945,7 +949,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..586c781c3 100644 --- a/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift +++ b/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift @@ -19,9 +19,13 @@ import Darwin import Bionic #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +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 a7e01952c..e1bfffa3f 100644 --- a/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift +++ b/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift @@ -14,11 +14,15 @@ @preconcurrency import Android #elseif canImport(Glibc) @preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl #elseif canImport(Darwin) @preconcurrency import Darwin #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif #if FOUNDATION_FRAMEWORK @@ -465,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 03ca025a2..9896b35a4 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift @@ -16,9 +16,13 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif #if os(Windows) @@ -83,14 +87,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 +131,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 +172,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..f8375b382 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift @@ -23,9 +23,13 @@ import Android import unistd #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +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 f8a770026..4b628c8da 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -23,9 +23,15 @@ 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 +#elseif os(WASI) +internal import _FoundationCShims +import WASILibc #endif extension Date { @@ -35,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 { @@ -120,8 +110,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 @@ -186,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 @@ -373,12 +363,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 @@ -461,7 +458,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), @@ -484,7 +481,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) @@ -555,7 +552,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 +564,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 +575,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) @@ -628,10 +635,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 { @@ -693,6 +701,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 { @@ -908,19 +919,31 @@ 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 - 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) } + #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) + #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 } if let date = attributes[.modificationDate] as? Date { diff --git a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift index fc9b8f70e..5d6c26f89 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift @@ -17,10 +17,14 @@ import Android import unistd #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK internal import _FoundationCShims +#elseif os(WASI) +import WASILibc #endif extension _FileManagerImpl { @@ -55,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/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift index bba8ed5c9..525bef766 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift @@ -28,9 +28,14 @@ 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 +#elseif os(WASI) +import WASILibc #endif #if os(Windows) @@ -173,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) @@ -270,20 +275,6 @@ extension _FileManagerImpl { } } #endif - -#if !os(Windows) - 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/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..14c6fd81d 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations.swift @@ -16,9 +16,13 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK +#elseif os(WASI) +import WASILibc #endif #if FOUNDATION_FRAMEWORK @@ -805,7 +809,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 +827,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) @@ -855,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 @@ -871,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/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/Formatting/BinaryInteger+NumericStringRepresentation.swift b/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift index db02789e2..663509deb 100644 --- a/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift +++ b/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift @@ -16,8 +16,12 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT +#elseif os(WASI) +import WASILibc #endif // MARK: - BinaryInteger + Numeric string representation 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..4e6aefa8a 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,10 +31,13 @@ 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 +#elseif os(WASI) + // WASI is single-threaded, so we don't need a lock. + typealias Primitive = Void #endif typealias PlatformLock = UnsafeMutablePointer @@ -45,6 +50,8 @@ package struct LockedState { pthread_mutex_init(platformLock, nil) #elseif canImport(WinSDK) InitializeSRWLock(platformLock) +#elseif os(WASI) + // no-op #endif } @@ -62,6 +69,8 @@ package struct LockedState { pthread_mutex_lock(platformLock) #elseif canImport(WinSDK) AcquireSRWLockExclusive(platformLock) +#elseif os(WASI) + // no-op #endif } @@ -72,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 20dc561af..6eb3accc4 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 { @@ -113,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) { @@ -134,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 @@ -174,7 +239,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) { @@ -191,7 +256,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 @@ -266,7 +331,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 302d3c62d..485f60681 100644 --- a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift +++ b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift @@ -19,8 +19,12 @@ import Bionic import unistd #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import WinSDK +#elseif os(WASI) +import WASILibc #endif #if !NO_PROCESS @@ -161,12 +165,11 @@ 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), - 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 } @@ -196,11 +199,10 @@ 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 { - return String(cString: fullname) + if let fullName = Platform.fullName(forUID: euid) { + return fullName } return "" #elseif os(WASI) @@ -389,7 +391,7 @@ extension _ProcessInfo { patch: Int(osVersionInfo.dwBuildNumber) ) #else - return OperatingSystemVersion(majorVersion: -1, minorVersion: 0, patchVersion: 0) + return (major: -1, minor: 0, patch: 0) #endif } @@ -433,7 +435,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 @@ -450,7 +452,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 @@ -544,7 +546,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) @@ -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..a484557c2 100644 --- a/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift +++ b/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift @@ -16,6 +16,10 @@ import Darwin import Bionic #elseif canImport(Glibc) import Glibc +#elseif os(WASI) +import WASILibc +#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..3185c1fdb 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -16,14 +16,18 @@ internal import os import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import WinSDK +#elseif os(WASI) +import WASILibc #endif 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 @@ -39,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)..([ " ", "/", @@ -186,23 +183,26 @@ 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 } - return 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 @@ -263,135 +269,188 @@ 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 + } - return String(cString: output) - } + enum SlashState { + case initial + case slash + } - #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(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 String(decoding: $0, as: UTF16.self) } } + 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]) + } + + 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 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 + } + + 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 +458,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 +481,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,9 +496,10 @@ 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 + #else // os(Windows) + #if targetEnvironment(simulator) if user == nil, let envValue = getenv("CFFIXED_USER_HOME") ?? getenv("HOME") { return String(cString: envValue).standardizingPath @@ -450,20 +511,21 @@ 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 { - 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 // !os(WASI) + // Fallback to HOME for the current user if possible if user == nil, let home = getenv("HOME") { return String(cString: home).standardizingPath @@ -471,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 @@ -510,76 +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 - } - /// 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 - } - } + #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 + }() - private 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]) - } - - 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 } @@ -591,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 } @@ -610,47 +654,29 @@ extension String { var standardizingPath: 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]) } @@ -676,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 { @@ -700,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. @@ -785,7 +810,7 @@ extension String { } } } -#endif + #endif // os(Windows) } var resolvingSymlinksInPath: String { @@ -795,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) @@ -825,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 { @@ -851,7 +880,7 @@ extension StringProtocol { } case .dotDot: if v == ._slash { - return true // Starts with "../" + return true // Starts with "../" or contains "/../" } else { state = .lookingForSlash } @@ -863,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/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 1243def34..ac61ee394 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 @@ -48,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 @@ -127,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) } } @@ -171,7 +175,7 @@ struct TimeZoneCache : Sendable { } } -#if os(Linux) && !os(WASI) +#if os(Linux) // Try localtime tzset() var t = time(nil) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index d23855889..9e197fd48 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`. @@ -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 } } @@ -1319,19 +1336,43 @@ public struct URL: Equatable, Sendable, Hashable { } } + 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 { - var result = urlPath - if result.count > 1 && result.utf8.last == UInt8(ascii: "/") { - _ = result.popLast() + let charsToLeaveEncoded: Set = [._slash, 0] + guard let posixPath = Parser.percentDecode(urlPath._droppingTrailingSlashes, excluding: charsToLeaveEncoded) else { + return "" } - let charsToLeaveEncoded = Set([UInt8(ascii: "/")]) - return Parser.percentDecode(result, excluding: charsToLeaveEncoded) ?? "" + #if os(Windows) + return windowsPath(for: posixPath) + #else + return posixPath + #endif } var fileSystemPath: String { 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`. @@ -1624,7 +1665,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 { @@ -1633,13 +1676,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)! } @@ -2026,55 +2086,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 +2181,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,49 +2195,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 !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: "/")) - #endif - - if !filePath.isEmpty && filePath.utf8.last != UInt8(ascii: "/") && isDirectory { + if isDirectory && !filePath.isEmpty && filePath.utf8.last != ._slash { filePath += "/" } var components = URLComponents() @@ -2193,9 +2244,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 @@ -2210,13 +2260,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 @@ -2446,6 +2513,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/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..ffe010c93 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,19 +26,21 @@ 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 #elseif canImport(WinSDK) fileprivate typealias PlatformKey = DWORD +#elseif os(WASI) + fileprivate typealias PlatformKey = UnsafeMutablePointer #endif struct Key { 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 @@ -46,28 +50,34 @@ struct _ThreadLocal { self.key = key #elseif canImport(WinSDK) key = FlsAlloc(nil) +#elseif os(WASI) + key = UnsafeMutablePointer.allocate(capacity: 1) #endif } } 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) #elseif canImport(WinSDK) FlsGetValue(key) +#elseif os(WASI) + key.pointee #endif } 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) #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 0f0b97386..01895b8b9 100644 --- a/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift +++ b/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift @@ -18,10 +18,14 @@ import FoundationEssentials import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(CRT) import CRT #elseif canImport(Darwin) import Darwin +#elseif os(WASI) +import WASILibc #endif internal import _FoundationICU 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..dfe2fad10 100644 --- a/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift +++ b/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift @@ -20,8 +20,12 @@ import Darwin import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +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/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/FoundationMacros/CMakeLists.txt b/Sources/FoundationMacros/CMakeLists.txt index 0cc31c299..7746d7bd6 100644 --- a/Sources/FoundationMacros/CMakeLists.txt +++ b/Sources/FoundationMacros/CMakeLists.txt @@ -12,29 +12,53 @@ ## ##===----------------------------------------------------------------------===## +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) + +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 -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 +68,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/Sources/_FoundationCShims/include/_CStdlib.h b/Sources/_FoundationCShims/include/_CStdlib.h index 8967eb7f4..5232967c9 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() @@ -142,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 */ @@ -149,6 +164,7 @@ #ifndef TZDEFAULT #define TZDEFAULT "/etc/localtime" #endif /* !defined TZDEFAULT */ +#endif /* TARGET_OS_MAC || TARGET_OS_LINUX */ #endif 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(); } diff --git a/Tests/FoundationEssentialsTests/DataIOTests.swift b/Tests/FoundationEssentialsTests/DataIOTests.swift index 5087168ac..cfe212d62 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 @@ -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/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/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..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 @@ -214,10 +218,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 +267,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) } + } } @@ -310,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 { @@ -345,14 +373,44 @@ 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 { 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) @@ -364,6 +422,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 { @@ -526,6 +619,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 +642,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 +965,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 +1000,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/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" 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) } diff --git a/Tests/FoundationEssentialsTests/StringTests.swift b/Tests/FoundationEssentialsTests/StringTests.swift index b5fff9aee..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() { @@ -812,6 +813,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 +848,43 @@ 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 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() { @@ -984,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 @@ -2504,17 +2569,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", diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 6366f1d2e..0998dd586 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"] @@ -330,6 +338,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") { @@ -352,6 +372,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")! @@ -536,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") @@ -554,6 +586,330 @@ 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///") + // 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 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 + 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() + // 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 { var comp = URLComponents() @@ -1014,4 +1370,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")