diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index 877967060..84fbf2050 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -71,11 +71,11 @@ switch usePackage { case .useGitHubPackage: #if os(macOS) - packageDependency.append(.package(url: "https://github.com/apple/swift-foundation", branch: "main")) + packageDependency.append(.package(url: "https://github.com/apple/swift-foundation", branch: "release/6.1")) targetDependency.append(.product(name: "FoundationEssentials", package: "swift-foundation")) targetDependency.append(.product(name: "FoundationInternationalization", package: "swift-foundation")) #else - packageDependency.append(.package(url: "https://github.com/apple/swift-corelibs-foundation", branch: "main")) + packageDependency.append(.package(url: "https://github.com/apple/swift-corelibs-foundation", branch: "release/6.1")) targetDependency.append(.product(name: "Foundation", package: "swift-corelibs-foundation")) #endif diff --git a/CMakeLists.txt b/CMakeLists.txt index 73c3e8805..a9a9e909d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,7 +68,7 @@ else() message(STATUS "_SwiftFoundationICU_SourceDIR not provided, checking out local copy of swift-foundation-icu") FetchContent_Declare(SwiftFoundationICU GIT_REPOSITORY https://github.com/apple/swift-foundation-icu.git - GIT_TAG 0.0.9) + GIT_TAG release/6.1) endif() if (_SwiftCollections_SourceDIR) diff --git a/Package.swift b/Package.swift index 9a6fcbd5f..995e2edca 100644 --- a/Package.swift +++ b/Package.swift @@ -62,10 +62,10 @@ var dependencies: [Package.Dependency] { from: "1.1.0"), .package( url: "https://github.com/apple/swift-foundation-icu", - branch: "main"), + branch: "release/6.1"), .package( url: "https://github.com/swiftlang/swift-syntax", - branch: "main") + branch: "release/6.1") ] } } diff --git a/Sources/FoundationEssentials/Calendar/Calendar.swift b/Sources/FoundationEssentials/Calendar/Calendar.swift index 35bd03ed5..ebbc5768d 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar.swift @@ -13,15 +13,15 @@ #if canImport(Darwin) internal import os #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif canImport(CRT) import CRT #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif #if FOUNDATION_FRAMEWORK diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift index 381c7a11d..959e0bf6b 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift @@ -13,15 +13,15 @@ #if canImport(os) internal import os #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif canImport(CRT) import CRT #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif diff --git a/Sources/FoundationEssentials/CodableUtilities.swift b/Sources/FoundationEssentials/CodableUtilities.swift index cffb8bbd1..fcdc15e98 100644 --- a/Sources/FoundationEssentials/CodableUtilities.swift +++ b/Sources/FoundationEssentials/CodableUtilities.swift @@ -13,9 +13,9 @@ #if canImport(Darwin) import Darwin #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif //===----------------------------------------------------------------------===// diff --git a/Sources/FoundationEssentials/Data/Data+Reading.swift b/Sources/FoundationEssentials/Data/Data+Reading.swift index e2ddae243..c94b25712 100644 --- a/Sources/FoundationEssentials/Data/Data+Reading.swift +++ b/Sources/FoundationEssentials/Data/Data+Reading.swift @@ -19,16 +19,16 @@ internal import _FoundationCShims #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Android +@preconcurrency import Android #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import CRT import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif func _fgetxattr(_ fd: Int32, _ name: UnsafePointer!, _ value: UnsafeMutableRawPointer!, _ size: Int, _ position: UInt32, _ options: Int32) -> Int { @@ -266,8 +266,8 @@ internal func readBytesFromFile(path inPath: PathOrURL, reportProgress: Bool, ma let szMapSize: UInt64 = min(UInt64(maxLength ?? Int.max), szFileSize) let pData: UnsafeMutableRawPointer = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, SIZE_T(szMapSize)) - return ReadBytesResult(bytes: pData, length: Int(szMapSize), deallocator: .custom({ hMapping, _ in - guard UnmapViewOfFile(hMapping) else { + return ReadBytesResult(bytes: pData, length: Int(szMapSize), deallocator: .custom({ pData, _ in + guard UnmapViewOfFile(pData) else { fatalError("UnmapViewOfFile") } guard CloseHandle(hMapping) else { diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 7c23e1eae..347757b42 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -20,35 +20,39 @@ internal import _FoundationCShims #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Android +@preconcurrency import Android import unistd #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import CRT import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif #if !NO_FILESYSTEM // MARK: - Helpers +#if os(Windows) +private func openFileDescriptorProtected(path: UnsafePointer, flags: Int32, options: Data.WritingOptions) -> Int32 { + var fd: CInt = 0 + _ = _wsopen_s(&fd, path, flags, _SH_DENYNO, _S_IREAD | _S_IWRITE) + return fd +} +#else private func openFileDescriptorProtected(path: UnsafePointer, flags: Int32, options: Data.WritingOptions) -> Int32 { #if FOUNDATION_FRAMEWORK // Use file protection on this platform return _NSOpenFileDescriptor_Protected(path, Int(flags), options, 0o666) -#elseif os(Windows) - var fd: CInt = 0 - _ = _sopen_s(&fd, path, flags, _SH_DENYNO, _S_IREAD | _S_IWRITE) - return fd #else return open(path, flags, 0o666) #endif } +#endif private func writeToFileDescriptorWithProgress(_ fd: Int32, buffer: UnsafeRawBufferPointer, reportProgress: Bool) throws -> Int { // Fetch this once @@ -159,18 +163,18 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL, // The warning diligently tells us we shouldn't be using mktemp() because blindly opening the returned path opens us up to a TOCTOU race. However, in this case, we're being careful by doing O_CREAT|O_EXCL and repeating, just like the implementation of mkstemp. // Furthermore, we can't compatibly switch to mkstemp() until we have the ability to set fchmod correctly, which requires the ability to query the current umask, which we don't have. (22033100) #if os(Windows) - guard _mktemp_s(templateFileSystemRep, template.count + 1) == 0 else { + guard _mktemp_s(templateFileSystemRep, strlen(templateFileSystemRep) + 1) == 0 else { throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant) } - let flags: CInt = _O_BINARY | _O_CREAT | _O_EXCL | _O_RDWR + let fd = String(cString: templateFileSystemRep).withCString(encodedAs: UTF16.self) { + openFileDescriptorProtected(path: $0, flags: _O_BINARY | _O_CREAT | _O_EXCL | _O_RDWR, options: options) + } #else guard mktemp(templateFileSystemRep) != nil else { throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant) } - let flags: CInt = O_CREAT | O_EXCL | O_RDWR + let fd = openFileDescriptorProtected(path: templateFileSystemRep, flags: O_CREAT | O_EXCL | O_RDWR, options: options) #endif - - let fd = openFileDescriptorProtected(path: templateFileSystemRep, flags: flags, options: options) if fd >= 0 { // Got a good fd return (fd, String(cString: templateFileSystemRep)) diff --git a/Sources/FoundationEssentials/Data/Data.swift b/Sources/FoundationEssentials/Data/Data.swift index 70df1e878..89d8c9468 100644 --- a/Sources/FoundationEssentials/Data/Data.swift +++ b/Sources/FoundationEssentials/Data/Data.swift @@ -18,7 +18,7 @@ @usableFromInline let memcpy = ucrt.memcpy @usableFromInline let memcmp = ucrt.memcmp #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic @usableFromInline let calloc = Bionic.calloc @usableFromInline let malloc = Bionic.malloc @usableFromInline let free = Bionic.free @@ -71,13 +71,13 @@ internal func malloc_good_size(_ size: Int) -> Int { #endif #if canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif canImport(ucrt) import ucrt #elseif canImport(WASILibc) -import WASILibc +@preconcurrency import WASILibc #endif #if os(Windows) diff --git a/Sources/FoundationEssentials/Data/DataProtocol.swift b/Sources/FoundationEssentials/Data/DataProtocol.swift index 92afe2a53..2f3667af8 100644 --- a/Sources/FoundationEssentials/Data/DataProtocol.swift +++ b/Sources/FoundationEssentials/Data/DataProtocol.swift @@ -13,13 +13,13 @@ #if canImport(Darwin) import Darwin #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif canImport(ucrt) import ucrt #elseif canImport(WASILibc) -import WASILibc +@preconcurrency import WASILibc #endif //===--- DataProtocol -----------------------------------------------------===// diff --git a/Sources/FoundationEssentials/Date.swift b/Sources/FoundationEssentials/Date.swift index 1e15baed4..74ecb0bd8 100644 --- a/Sources/FoundationEssentials/Date.swift +++ b/Sources/FoundationEssentials/Date.swift @@ -13,15 +13,15 @@ #if canImport(Darwin) import Darwin #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif canImport(WinSDK) import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif #if !FOUNDATION_FRAMEWORK diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift index dffeea2a2..32e68359b 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift @@ -13,15 +13,15 @@ #if canImport(Darwin) import Darwin #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif canImport(CRT) import CRT #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif private let powerOfTen: [Decimal.VariableLengthInteger] = [ diff --git a/Sources/FoundationEssentials/Decimal/Decimal.swift b/Sources/FoundationEssentials/Decimal/Decimal.swift index 8c02272d6..aa4771ddd 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal.swift @@ -13,7 +13,7 @@ #if canImport(Darwin) import Darwin #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(ucrt) import ucrt #endif diff --git a/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift b/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift index a387f8779..c6d8ef97e 100644 --- a/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift +++ b/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift @@ -16,16 +16,16 @@ internal import _ForSwiftFoundation #if canImport(Darwin) import Darwin #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import CRT import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif // MARK: - Error Creation with CocoaError.Code diff --git a/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift b/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift index fa521e7cb..5c9c47ae4 100644 --- a/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift +++ b/Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift @@ -22,7 +22,7 @@ import CRT import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif #if FOUNDATION_FRAMEWORK diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift b/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift index eee25c849..92d7fa859 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Basics.swift @@ -13,16 +13,16 @@ #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Android +@preconcurrency import Android #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import CRT import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif #if os(Windows) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift index 7d74d8636..41bb83eb0 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift @@ -19,17 +19,17 @@ internal import os #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Android +@preconcurrency import Android import unistd #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import CRT import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif internal import _FoundationCShims @@ -48,7 +48,13 @@ extension _FileManagerImpl { } func homeDirectory(forUser userName: String?) -> URL? { - URL(filePath: String.homeDirectoryPath(forUser: userName), directoryHint: .isDirectory) + guard let userName else { + return homeDirectoryForCurrentUser + } + guard let path = String.homeDirectoryPath(forUser: userName) else { + return nil + } + return URL(filePath: path, directoryHint: .isDirectory) } var temporaryDirectory: URL { diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index 7a0b35e8b..64f8e21b1 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -18,20 +18,20 @@ internal import DarwinPrivate.sys.content_protection #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Android +@preconcurrency import Android import posix_filesystem #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc internal import _FoundationCShims #elseif canImport(Musl) -import Musl +@preconcurrency import Musl internal import _FoundationCShims #elseif os(Windows) import CRT import WinSDK #elseif os(WASI) internal import _FoundationCShims -import WASILibc +@preconcurrency import WASILibc #endif extension Date { diff --git a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift index 5226edfd6..5e4e00b12 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift @@ -13,18 +13,18 @@ #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Android +@preconcurrency import Android import unistd #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import CRT import WinSDK internal import _FoundationCShims #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif extension _FileManagerImpl { diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift index 0d34680ae..786751d73 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift @@ -24,18 +24,18 @@ internal import QuarantinePrivate #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Android +@preconcurrency import Android #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc internal import _FoundationCShims #elseif canImport(Musl) -import Musl +@preconcurrency import Musl internal import _FoundationCShims #elseif os(Windows) import CRT import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif #if os(Windows) diff --git a/Sources/FoundationEssentials/FileManager/FileOperations+Enumeration.swift b/Sources/FoundationEssentials/FileManager/FileOperations+Enumeration.swift index 81ed95697..185dfcb79 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations+Enumeration.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations+Enumeration.swift @@ -115,15 +115,15 @@ struct _Win32DirectoryContentsSequence: Sequence { #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Android +@preconcurrency import Android import posix_filesystem.dirent #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc internal import _FoundationCShims #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc internal import _FoundationCShims #endif diff --git a/Sources/FoundationEssentials/FileManager/FileOperations.swift b/Sources/FoundationEssentials/FileManager/FileOperations.swift index 6f228d808..09aaf938b 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations.swift @@ -13,16 +13,16 @@ #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Android +@preconcurrency import Android #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import CRT import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif #if FOUNDATION_FRAMEWORK @@ -908,6 +908,97 @@ enum _FileOperations { #endif } #endif + + #if !canImport(Darwin) + private static func _copyDirectoryMetadata(srcFD: CInt, srcPath: @autoclosure () -> String, dstFD: CInt, dstPath: @autoclosure () -> String, delegate: some LinkOrCopyDelegate) throws { + #if !os(WASI) && !os(Android) + // Copy extended attributes + var size = flistxattr(srcFD, nil, 0) + if size > 0 { + try withUnsafeTemporaryAllocation(of: CChar.self, capacity: size) { keyList in + size = flistxattr(srcFD, keyList.baseAddress!, size) + if size > 0 { + var current = keyList.baseAddress! + let end = keyList.baseAddress!.advanced(by: keyList.count) + while current < end { + var valueSize = fgetxattr(srcFD, current, nil, 0) + if valueSize >= 0 { + try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: valueSize) { valueBuffer in + valueSize = fgetxattr(srcFD, current, valueBuffer.baseAddress!, valueSize) + if valueSize >= 0 { + if fsetxattr(dstFD, current, valueBuffer.baseAddress!, valueSize, 0) != 0 { + try delegate.throwIfNecessary(errno, srcPath(), dstPath()) + } + } + } + } + current = current.advanced(by: strlen(current) + 1) /* pass null byte */ + } + } + } + } + #endif + var statInfo = stat() + if fstat(srcFD, &statInfo) == 0 { + #if !os(WASI) // WASI doesn't have fchown for now + // Copy owner/group + if fchown(dstFD, statInfo.st_uid, statInfo.st_gid) != 0 { + try delegate.throwIfNecessary(errno, srcPath(), dstPath()) + } + #endif + + // Copy modification date + let value = statInfo.st_mtim + var tv = (value, value) + try withUnsafePointer(to: &tv) { + try $0.withMemoryRebound(to: timespec.self, capacity: 2) { + if futimens(dstFD, $0) != 0 { + try delegate.throwIfNecessary(errno, srcPath(), dstPath()) + } + } + } + + #if !os(WASI) // WASI doesn't have fchmod for now + // Copy permissions + if fchmod(dstFD, mode_t(statInfo.st_mode)) != 0 { + try delegate.throwIfNecessary(errno, srcPath(), dstPath()) + } + #endif + } else { + try delegate.throwIfNecessary(errno, srcPath(), dstPath()) + } + } + #endif + + private static func _openDirectoryFD(_ ptr: UnsafePointer, srcPath: @autoclosure () -> String, dstPath: @autoclosure () -> String, delegate: some LinkOrCopyDelegate) throws -> CInt? { + let fd = open(ptr, O_RDONLY | O_NOFOLLOW | O_DIRECTORY) + guard fd >= 0 else { + try delegate.throwIfNecessary(errno, srcPath(), dstPath()) + return nil + } + return fd + } + + // Safely copies metadata from one directory to another ensuring that both paths are directories and cannot be swapped for files before/while copying metadata + private static func _safeCopyDirectoryMetadata(src: UnsafePointer, dst: UnsafePointer, delegate: some LinkOrCopyDelegate, extraFlags: Int32 = 0) throws { + guard let srcFD = try _openDirectoryFD(src, srcPath: String(cString: src), dstPath: String(cString: dst), delegate: delegate) else { + return + } + defer { close(srcFD) } + + guard let dstFD = try _openDirectoryFD(dst, srcPath: String(cString: src), dstPath: String(cString: dst), delegate: delegate) else { + return + } + defer { close(dstFD) } + + #if canImport(Darwin) + if fcopyfile(srcFD, dstFD, nil, copyfile_flags_t(COPYFILE_METADATA | COPYFILE_NOFOLLOW | extraFlags)) != 0 { + try delegate.throwIfNecessary(errno, String(cString: src), String(cString: dst)) + } + #else + try _copyDirectoryMetadata(srcFD: srcFD, srcPath: String(cString: src), dstFD: dstFD, dstPath: String(cString: dst), delegate: delegate) + #endif + } #if os(WASI) private static func _linkOrCopyFile(_ srcPtr: UnsafePointer, _ dstPtr: UnsafePointer, with fileManager: FileManager, delegate: some LinkOrCopyDelegate) throws { @@ -1000,18 +1091,7 @@ enum _FileOperations { case FTS_DP: // Directory being visited in post-order - copy the permissions over. - #if canImport(Darwin) - if copyfile(fts_path, buffer.baseAddress!, nil, copyfile_flags_t(COPYFILE_METADATA | COPYFILE_NOFOLLOW | extraFlags)) != 0 { - try delegate.throwIfNecessary(errno, String(cString: fts_path), String(cString: buffer.baseAddress!)) - } - #else - do { - let attributes = try fileManager.attributesOfItem(atPath: String(cString: fts_path)) - try fileManager.setAttributes(attributes, ofItemAtPath: String(cString: buffer.baseAddress!)) - } catch { - try delegate.throwIfNecessary(error, String(cString: fts_path), String(cString: buffer.baseAddress!)) - } - #endif + try Self._safeCopyDirectoryMetadata(src: fts_path, dst: buffer.baseAddress!, delegate: delegate, extraFlags: extraFlags) case FTS_SL: fallthrough // Symlink. case FTS_SLNONE: // Symlink with no target. diff --git a/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift b/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift index 7225198a9..aa03a8c6e 100644 --- a/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift +++ b/Sources/FoundationEssentials/Formatting/BinaryInteger+NumericStringRepresentation.swift @@ -13,15 +13,15 @@ #if canImport(Darwin) import Darwin #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import CRT #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif // MARK: - BinaryInteger + Numeric string representation diff --git a/Sources/FoundationEssentials/JSON/JSON5Scanner.swift b/Sources/FoundationEssentials/JSON/JSON5Scanner.swift index c89054bcf..10d1af10b 100644 --- a/Sources/FoundationEssentials/JSON/JSON5Scanner.swift +++ b/Sources/FoundationEssentials/JSON/JSON5Scanner.swift @@ -13,7 +13,7 @@ #if canImport(Darwin) import Darwin #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif internal import _FoundationCShims diff --git a/Sources/FoundationEssentials/JSON/JSONDecoder.swift b/Sources/FoundationEssentials/JSON/JSONDecoder.swift index b7c629012..765103b1d 100644 --- a/Sources/FoundationEssentials/JSON/JSONDecoder.swift +++ b/Sources/FoundationEssentials/JSON/JSONDecoder.swift @@ -13,7 +13,7 @@ #if canImport(Darwin) import Darwin #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif internal import _FoundationCShims diff --git a/Sources/FoundationEssentials/JSON/JSONScanner.swift b/Sources/FoundationEssentials/JSON/JSONScanner.swift index 780abd5ba..f78c3637b 100644 --- a/Sources/FoundationEssentials/JSON/JSONScanner.swift +++ b/Sources/FoundationEssentials/JSON/JSONScanner.swift @@ -56,7 +56,7 @@ #if canImport(Darwin) import Darwin #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif // canImport(Darwin) internal import _FoundationCShims diff --git a/Sources/FoundationEssentials/Locale/Locale+Components.swift b/Sources/FoundationEssentials/Locale/Locale+Components.swift index 42fce9df9..a093b21ff 100644 --- a/Sources/FoundationEssentials/Locale/Locale+Components.swift +++ b/Sources/FoundationEssentials/Locale/Locale+Components.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// #if canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif extension Locale { diff --git a/Sources/FoundationEssentials/LockedState.swift b/Sources/FoundationEssentials/LockedState.swift index ac7539f1b..e17bb9a1a 100644 --- a/Sources/FoundationEssentials/LockedState.swift +++ b/Sources/FoundationEssentials/LockedState.swift @@ -16,11 +16,11 @@ internal import os internal import C.os.lock #endif #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif canImport(WinSDK) import WinSDK #endif @@ -46,17 +46,19 @@ package struct LockedState { fileprivate static func initialize(_ platformLock: PlatformLock) { #if canImport(os) platformLock.initialize(to: os_unfair_lock()) -#elseif canImport(Bionic) || canImport(Glibc) +#elseif canImport(Bionic) || canImport(Glibc) || canImport(Musl) pthread_mutex_init(platformLock, nil) #elseif canImport(WinSDK) InitializeSRWLock(platformLock) #elseif os(WASI) // no-op +#else +#error("LockedState._Lock.initialize is unimplemented on this platform") #endif } fileprivate static func deinitialize(_ platformLock: PlatformLock) { -#if canImport(Bionic) || canImport(Glibc) +#if canImport(Bionic) || canImport(Glibc) || canImport(Musl) pthread_mutex_destroy(platformLock) #endif platformLock.deinitialize(count: 1) @@ -65,24 +67,28 @@ package struct LockedState { static fileprivate func lock(_ platformLock: PlatformLock) { #if canImport(os) os_unfair_lock_lock(platformLock) -#elseif canImport(Bionic) || canImport(Glibc) +#elseif canImport(Bionic) || canImport(Glibc) || canImport(Musl) pthread_mutex_lock(platformLock) #elseif canImport(WinSDK) AcquireSRWLockExclusive(platformLock) #elseif os(WASI) // no-op +#else +#error("LockedState._Lock.lock is unimplemented on this platform") #endif } static fileprivate func unlock(_ platformLock: PlatformLock) { #if canImport(os) os_unfair_lock_unlock(platformLock) -#elseif canImport(Bionic) || canImport(Glibc) +#elseif canImport(Bionic) || canImport(Glibc) || canImport(Musl) pthread_mutex_unlock(platformLock) #elseif canImport(WinSDK) ReleaseSRWLockExclusive(platformLock) #elseif os(WASI) // no-op +#else +#error("LockedState._Lock.unlock is unimplemented on this platform") #endif } } diff --git a/Sources/FoundationEssentials/Platform.swift b/Sources/FoundationEssentials/Platform.swift index 15a1362f8..5177da6ca 100644 --- a/Sources/FoundationEssentials/Platform.swift +++ b/Sources/FoundationEssentials/Platform.swift @@ -29,13 +29,13 @@ fileprivate let _pageSize: Int = { // WebAssembly defines a fixed page size fileprivate let _pageSize: Int = 65_536 #elseif canImport(Android) -import Android +@preconcurrency import Android fileprivate let _pageSize: Int = Int(getpagesize()) #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc fileprivate let _pageSize: Int = Int(getpagesize()) #elseif canImport(Musl) -import Musl +@preconcurrency import Musl fileprivate let _pageSize: Int = Int(getpagesize()) #elseif canImport(C) fileprivate let _pageSize: Int = Int(getpagesize()) diff --git a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift index 22f2ac068..c05e912cb 100644 --- a/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift +++ b/Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift @@ -15,16 +15,16 @@ internal import _FoundationCShims #if canImport(Darwin) import Darwin #elseif canImport(Android) -import Bionic +@preconcurrency import Bionic import unistd #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif #if !NO_PROCESS diff --git a/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift b/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift index 3f45ec034..3a40bfe4d 100644 --- a/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift +++ b/Sources/FoundationEssentials/PropertyList/OpenStepPlist.swift @@ -13,13 +13,13 @@ #if canImport(Darwin) import Darwin #elseif canImport(Bionic) -import Bionic +@preconcurrency import Bionic #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif #if canImport(CRT) diff --git a/Sources/FoundationEssentials/String/String+IO.swift b/Sources/FoundationEssentials/String/String+IO.swift index 4adc11ba4..3513cac7e 100644 --- a/Sources/FoundationEssentials/String/String+IO.swift +++ b/Sources/FoundationEssentials/String/String+IO.swift @@ -18,33 +18,6 @@ internal import _FoundationCShims fileprivate let stringEncodingAttributeName = "com.apple.TextEncoding" -private struct ExtendingToUTF16Sequence> : Sequence { - typealias Element = UInt16 - - struct Iterator : IteratorProtocol { - private var base: Base.Iterator - - init(_ base: Base.Iterator) { - self.base = base - } - - mutating func next() -> Element? { - guard let value = base.next() else { return nil } - return UInt16(value) - } - } - - private let base: Base - - init(_ base: Base) { - self.base = base - } - - func makeIterator() -> Iterator { - Iterator(base.makeIterator()) - } -} - @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension String { @@ -181,12 +154,9 @@ extension String { } #if !FOUNDATION_FRAMEWORK case .isoLatin1: - guard bytes.allSatisfy(\.isValidISOLatin1) else { - return nil - } - // isoLatin1 is an 8-bit encoding that represents a subset of UTF-16 - // Map to 16-bit values and decode as UTF-16 - self.init(_validating: ExtendingToUTF16Sequence(bytes), as: UTF16.self) + // ISO Latin 1 bytes are always valid since it's an 8-bit encoding that maps scalars 0x0 through 0xFF + // Simply extend each byte to 16 bits and decode as UTF-16 + self.init(decoding: bytes.lazy.map { UInt16($0) }, as: UTF16.self) case .macOSRoman: func buildString(_ bytes: UnsafeBufferPointer) -> String { String(unsafeUninitializedCapacity: bytes.count * 3) { buffer in diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index a2ebfe826..df3a97433 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -13,15 +13,15 @@ #if canImport(Darwin) internal import os #elseif canImport(Android) -import Android +@preconcurrency import Android #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) import WinSDK #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif internal import _FoundationCShims @@ -207,7 +207,7 @@ extension String { } internal func appendingPathExtension(_ pathExtension: String) -> String { - guard validatePathExtension(pathExtension) else { + guard !pathExtension.isEmpty, validatePathExtension(pathExtension) else { return self } var result = self._droppingTrailingSlashes @@ -422,86 +422,42 @@ extension String { // MARK: - Filesystem String Extensions #if !NO_FILESYSTEM - - internal static func homeDirectoryPath(forUser user: String? = nil) -> String { + + internal static func homeDirectoryPath() -> 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() + func fallbackCurrentUserDirectory() -> String { + guard let fallback = ProcessInfo.processInfo.environment["ALLUSERSPROFILE"] else { + fatalError("Unable to find home directory for current user and ALLUSERSPROFILE environment variable is not set") } - return user.withCString(encodedAs: UTF16.self) { pwszUserName in - var cbSID: DWORD = 0 - var cchReferencedDomainName: DWORD = 0 - var eUse: SID_NAME_USE = SidTypeUnknown - 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 { - return fallbackUserDirectory() - } - - var pwszSID: LPWSTR? = nil - guard ConvertSidToStringSidW(pSID.baseAddress, &pwszSID) else { - 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 "ProfileImagePath".withCString(encodedAs: UTF16.self) { pwszKey in - var cbData: DWORD = 0 - 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 - guard RegGetValueW(HKEY_LOCAL_MACHINE, pwszKeyPath, pwszKey, RRF_RT_REG_SZ, nil, pwszData.baseAddress, &cbData) == ERROR_SUCCESS else { - fatalError("unable to query ProfileImagePath for user \(user)") - } - return String(decodingCString: pwszData.baseAddress!, as: UTF16.self) - } - } - } - - } - } - } + return fallback } - + var hToken: HANDLE? = nil guard OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken) else { guard let UserProfile = ProcessInfo.processInfo.environment["UserProfile"] else { - fatalError("unable to evaluate `%UserProfile%`") + return fallbackCurrentUserDirectory() } return UserProfile } defer { CloseHandle(hToken) } - + var dwcchSize: DWORD = 0 _ = GetUserProfileDirectoryW(hToken, nil, &dwcchSize) - + return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwcchSize)) { var dwcchSize: DWORD = DWORD($0.count) guard GetUserProfileDirectoryW(hToken, $0.baseAddress, &dwcchSize) else { - fatalError("unable to query user profile directory") + return fallbackCurrentUserDirectory() } return String(decodingCString: $0.baseAddress!, as: UTF16.self) } - #else // os(Windows) - + #else + + #if targetEnvironment(simulator) - if user == nil, let envValue = getenv("CFFIXED_USER_HOME") ?? getenv("HOME") { + // Simulator checks these environment variables first for the current user + if let envValue = getenv("CFFIXED_USER_HOME") ?? getenv("HOME") { return String(cString: envValue).standardizingPath } #endif @@ -512,28 +468,81 @@ extension String { } #if !os(WASI) // WASI does not have user concept - // Next, attempt to find the home directory via getpwnam/getpwuid - if let 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) - if let dir = Platform.homeDirectory(forUID: Platform.getUGIDs(allowEffectiveRootUID: false).uid) { - return dir.standardizingPath - } + // Next, attempt to find the home directory via getpwuid + // 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) + if let dir = Platform.homeDirectory(forUID: Platform.getUGIDs(allowEffectiveRootUID: false).uid) { + return dir.standardizingPath } - #endif // !os(WASI) - + #endif + // Fallback to HOME for the current user if possible - if user == nil, let home = getenv("HOME") { + if let home = getenv("HOME") { return String(cString: home).standardizingPath } - // If all else fails, log and fall back to /var/empty + // If all else fails, fall back to /var/empty return "/var/empty" - #endif // os(Windows) + #endif + } + + internal static func homeDirectoryPath(forUser user: String) -> String? { + #if os(Windows) + guard !user.isEmpty else { + return nil + } + + return user.withCString(encodedAs: UTF16.self) { pwszUserName in + var cbSID: DWORD = 0 + var cchReferencedDomainName: DWORD = 0 + var eUse: SID_NAME_USE = SidTypeUnknown + LookupAccountNameW(nil, pwszUserName, nil, &cbSID, nil, &cchReferencedDomainName, &eUse) + guard cbSID > 0 else { + return nil + } + + 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 { + return nil + } + + var pwszSID: LPWSTR? = nil + guard ConvertSidToStringSidW(pSID.baseAddress, &pwszSID) else { + 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 "ProfileImagePath".withCString(encodedAs: UTF16.self) { pwszKey in + var cbData: DWORD = 0 + 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 + guard RegGetValueW(HKEY_LOCAL_MACHINE, pwszKeyPath, pwszKey, RRF_RT_REG_SZ, nil, pwszData.baseAddress, &cbData) == ERROR_SUCCESS else { + fatalError("unable to query ProfileImagePath for user \(user)") + } + return String(decodingCString: pwszData.baseAddress!, as: UTF16.self) + } + } + } + + } + } + } + #else + // First check CFFIXED_USER_HOME if the environment is not considered tainted + if let envVar = Platform.getEnvSecure("CFFIXED_USER_HOME") { + return envVar.standardizingPath + } + #if !os(WASI) // WASI does not have user concept + // Next, attempt to find the home directory via getpwnam + return Platform.homeDirectory(forUserName: user)?.standardizingPath + #else + return nil + #endif + #endif } // From swift-corelibs-foundation's NSTemporaryDirectory. Internal for now, pending a better public API. @@ -674,13 +683,17 @@ extension String { private var _expandingTildeInPath: String { guard utf8.first == UInt8(ascii: "~") else { return self } - var user: String? = nil let firstSlash = utf8.firstIndex(of: ._slash) ?? endIndex let indexAfterTilde = utf8.index(after: startIndex) + var userDir: String if firstSlash != indexAfterTilde { - user = String(self[indexAfterTilde ..< firstSlash]) + guard let dir = String.homeDirectoryPath(forUser: String(self[indexAfterTilde ..< firstSlash])) else { + return self + } + userDir = dir + } else { + userDir = String.homeDirectoryPath() } - let userDir = String.homeDirectoryPath(forUser: user) return userDir + self[firstSlash...] } diff --git a/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift b/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift index c86c6e664..4da60c1a6 100644 --- a/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift +++ b/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift @@ -16,17 +16,11 @@ internal import _ForSwiftFoundation #if canImport(Darwin) import Darwin #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif internal import _FoundationCShims -extension BinaryInteger { - var isValidISOLatin1: Bool { - (0x20 <= self && self <= 0x7E) || (0xA0 <= self && self <= 0xFF) - } -} - extension UInt8 { private typealias UTF8Representation = (UInt8, UInt8, UInt8) private static func withMacRomanMap(_ body: (UnsafeBufferPointer) -> R) -> R { @@ -228,12 +222,14 @@ extension String { return data + swapped #if !FOUNDATION_FRAMEWORK case .isoLatin1: - return try? Data(capacity: self.utf16.count) { buffer in - for scalar in self.utf16 { - guard scalar.isValidISOLatin1 else { + // ISO Latin 1 encodes code points 0x0 through 0xFF (a maximum of 2 UTF-8 scalars per ISO Latin 1 Scalar) + // The UTF-8 count is a cheap, reasonable starting capacity as it is precise for the all-ASCII case and it will only over estimate by 1 byte per non-ASCII character + return try? Data(capacity: self.utf8.count) { buffer in + for scalar in self.unicodeScalars { + guard let valid = UInt8(exactly: scalar.value) else { throw CocoaError(.fileWriteInapplicableStringEncoding) } - buffer.appendElement(UInt8(scalar & 0xFF)) + buffer.appendElement(valid) } } case .macOSRoman: diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone.swift b/Sources/FoundationEssentials/TimeZone/TimeZone.swift index fa26a8c7a..4a6e50c96 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone.swift @@ -13,7 +13,7 @@ #if canImport(Darwin) import Darwin #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif internal import _FoundationCShims diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift index ccbb69aae..b28c25f82 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift @@ -15,9 +15,9 @@ import Darwin #elseif canImport(Android) import unistd #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif canImport(ucrt) import ucrt #endif diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index 27ca1b969..39ddb6bc0 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -763,6 +763,10 @@ public struct URL: Equatable, Sendable, Hashable { internal var _parseInfo: URLParseInfo! private var _baseParseInfo: URLParseInfo? + private static func parse(urlString: String, encodingInvalidCharacters: Bool = true) -> URLParseInfo? { + return Parser.parse(urlString: urlString, encodingInvalidCharacters: encodingInvalidCharacters, compatibility: .allowEmptyScheme) + } + internal init(parseInfo: URLParseInfo, relativeTo url: URL? = nil) { _parseInfo = parseInfo if parseInfo.scheme == nil { @@ -773,26 +777,44 @@ public struct URL: Equatable, Sendable, Hashable { #endif // FOUNDATION_FRAMEWORK } + /// The public initializers don't allow the empty string, and we must maintain that behavior + /// for compatibility. However, there are cases internally where we need to create a URL with + /// an empty string, such as when `.deletingLastPathComponent()` of a single path + /// component. This previously worked since `URL` just wrapped an `NSURL`, which + /// allows the empty string. + internal init?(stringOrEmpty: String, relativeTo url: URL? = nil) { + #if FOUNDATION_FRAMEWORK + guard foundation_swift_url_enabled() else { + guard let inner = NSURL(string: stringOrEmpty, relativeTo: url) else { return nil } + _url = URL._converted(from: inner) + return + } + #endif // FOUNDATION_FRAMEWORK + guard let parseInfo = URL.parse(urlString: stringOrEmpty) else { + return nil + } + _parseInfo = parseInfo + if parseInfo.scheme == nil { + _baseParseInfo = url?.absoluteURL._parseInfo + } + #if FOUNDATION_FRAMEWORK + _url = URL._nsURL(from: _parseInfo, baseParseInfo: _baseParseInfo) + #endif // FOUNDATION_FRAMEWORK + } + /// Initialize with string. /// /// Returns `nil` if a `URL` cannot be formed with the string (for example, if the string contains characters that are illegal in a URL, or is an empty string). public init?(string: __shared String) { + guard !string.isEmpty else { return nil } #if FOUNDATION_FRAMEWORK guard foundation_swift_url_enabled() else { - guard !string.isEmpty, let inner = NSURL(string: string) else { return nil } + guard let inner = NSURL(string: string) else { return nil } _url = URL._converted(from: inner) return } - // Linked-on-or-after check for apps which pass an empty string. - // The new URL(string:) implementations allow the empty string - // as input since an empty path is valid and can be resolved - // against a base URL. This is shown in the RFC 3986 examples: - // https://datatracker.ietf.org/doc/html/rfc3986#section-5.4.1 - if Self.compatibility1 && string.isEmpty { - return nil - } #endif // FOUNDATION_FRAMEWORK - guard let parseInfo = Parser.parse(urlString: string, encodingInvalidCharacters: true) else { + guard let parseInfo = URL.parse(urlString: string) else { return nil } _parseInfo = parseInfo @@ -805,14 +827,15 @@ public struct URL: Equatable, Sendable, Hashable { /// /// Returns `nil` if a `URL` cannot be formed with the string (for example, if the string contains characters that are illegal in a URL, or is an empty string). public init?(string: __shared String, relativeTo url: __shared URL?) { + guard !string.isEmpty else { return nil } #if FOUNDATION_FRAMEWORK guard foundation_swift_url_enabled() else { - guard !string.isEmpty, let inner = NSURL(string: string, relativeTo: url) else { return nil } + guard let inner = NSURL(string: string, relativeTo: url) else { return nil } _url = URL._converted(from: inner) return } #endif // FOUNDATION_FRAMEWORK - guard let parseInfo = Parser.parse(urlString: string, encodingInvalidCharacters: true) else { + guard let parseInfo = URL.parse(urlString: string) else { return nil } _parseInfo = parseInfo @@ -831,14 +854,15 @@ public struct URL: Equatable, Sendable, Hashable { /// If the URL string is still invalid after encoding, `nil` is returned. @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) public init?(string: __shared String, encodingInvalidCharacters: Bool) { + guard !string.isEmpty else { return nil } #if FOUNDATION_FRAMEWORK guard foundation_swift_url_enabled() else { - guard !string.isEmpty, let inner = NSURL(string: string, encodingInvalidCharacters: encodingInvalidCharacters) else { return nil } + guard let inner = NSURL(string: string, encodingInvalidCharacters: encodingInvalidCharacters) else { return nil } _url = URL._converted(from: inner) return } #endif // FOUNDATION_FRAMEWORK - guard let parseInfo = Parser.parse(urlString: string, encodingInvalidCharacters: encodingInvalidCharacters) else { + guard let parseInfo = URL.parse(urlString: string, encodingInvalidCharacters: encodingInvalidCharacters) else { return nil } _parseInfo = parseInfo @@ -865,7 +889,7 @@ public struct URL: Equatable, Sendable, Hashable { } #endif let directoryHint: DirectoryHint = isDirectory ? .isDirectory : .notDirectory - self.init(filePath: path, directoryHint: directoryHint, relativeTo: base) + self.init(filePath: path.isEmpty ? "." : path, directoryHint: directoryHint, relativeTo: base) } /// Initializes a newly created file URL referencing the local file or directory at path, relative to a base URL. @@ -884,7 +908,7 @@ public struct URL: Equatable, Sendable, Hashable { return } #endif - self.init(filePath: path, directoryHint: .checkFileSystem, relativeTo: base) + self.init(filePath: path.isEmpty ? "." : path, directoryHint: .checkFileSystem, relativeTo: base) } /// Initializes a newly created file URL referencing the local file or directory at path. @@ -905,7 +929,7 @@ public struct URL: Equatable, Sendable, Hashable { } #endif let directoryHint: DirectoryHint = isDirectory ? .isDirectory : .notDirectory - self.init(filePath: path, directoryHint: directoryHint) + self.init(filePath: path.isEmpty ? "." : path, directoryHint: directoryHint) } /// Initializes a newly created file URL referencing the local file or directory at path. @@ -924,7 +948,7 @@ public struct URL: Equatable, Sendable, Hashable { return } #endif - self.init(filePath: path, directoryHint: .checkFileSystem) + self.init(filePath: path.isEmpty ? "." : path, directoryHint: .checkFileSystem) } // NSURL(fileURLWithPath:) can return nil incorrectly for some malformed paths @@ -948,24 +972,24 @@ public struct URL: Equatable, Sendable, Hashable { /// /// If the data representation is not a legal URL string as ASCII bytes, the URL object may not behave as expected. If the URL cannot be formed then this will return nil. @available(macOS 10.11, iOS 9.0, watchOS 2.0, tvOS 9.0, *) - public init?(dataRepresentation: __shared Data, relativeTo url: __shared URL?, isAbsolute: Bool = false) { + public init?(dataRepresentation: __shared Data, relativeTo base: __shared URL?, isAbsolute: Bool = false) { guard !dataRepresentation.isEmpty else { return nil } #if FOUNDATION_FRAMEWORK guard foundation_swift_url_enabled() else { if isAbsolute { - _url = URL._converted(from: NSURL(absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: url)) + _url = URL._converted(from: NSURL(absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: base)) } else { - _url = URL._converted(from: NSURL(dataRepresentation: dataRepresentation, relativeTo: url)) + _url = URL._converted(from: NSURL(dataRepresentation: dataRepresentation, relativeTo: base)) } return } #endif var url: URL? if let string = String(data: dataRepresentation, encoding: .utf8) { - url = URL(string: string, relativeTo: url) + url = URL(stringOrEmpty: string, relativeTo: base) } if url == nil, let string = String(data: dataRepresentation, encoding: .isoLatin1) { - url = URL(string: string, relativeTo: url) + url = URL(stringOrEmpty: string, relativeTo: base) } guard let url else { return nil @@ -990,7 +1014,7 @@ public struct URL: Equatable, Sendable, Hashable { return } #endif - guard let parseInfo = Parser.parse(urlString: _url.relativeString, encodingInvalidCharacters: true) else { + guard let parseInfo = URL.parse(urlString: _url.relativeString) else { return nil } _parseInfo = parseInfo @@ -1011,7 +1035,7 @@ public struct URL: Equatable, Sendable, Hashable { } #endif bookmarkDataIsStale = stale.boolValue - let parseInfo = Parser.parse(urlString: _url.relativeString, encodingInvalidCharacters: true)! + let parseInfo = URL.parse(urlString: _url.relativeString)! _parseInfo = parseInfo if parseInfo.scheme == nil { _baseParseInfo = url?.absoluteURL._parseInfo @@ -1089,7 +1113,9 @@ public struct URL: Equatable, Sendable, Hashable { } if let baseScheme = _baseParseInfo.scheme { - result.scheme = String(baseScheme) + // Scheme might be empty, which URL allows for compatibility, + // but URLComponents does not, so we force it internally. + result.forceScheme(String(baseScheme)) } if hasAuthority { @@ -1236,6 +1262,14 @@ public struct URL: Equatable, Sendable, Hashable { return nil } + // According to RFC 3986, a host always exists if there is an authority + // component, it just might be empty. However, the old implementation + // of URL.host() returned nil for URLs like "https:///", and apps rely + // on this behavior, so keep it for bincompat. + if encodedHost.isEmpty, user() == nil, password() == nil, port == nil { + return nil + } + func requestedHost() -> String? { let didPercentEncodeHost = hasAuthority ? _parseInfo.didPercentEncodeHost : _baseParseInfo?.didPercentEncodeHost ?? false if percentEncoded { @@ -1456,7 +1490,7 @@ public struct URL: Equatable, Sendable, Hashable { } #endif if _baseParseInfo != nil { - return absoluteURL.path(percentEncoded: percentEncoded) + return absoluteURL.relativePath(percentEncoded: percentEncoded) } if percentEncoded { return String(_parseInfo.path) @@ -1754,7 +1788,7 @@ public struct URL: Equatable, Sendable, Hashable { return result } #endif - guard !relativePath().isEmpty else { return self } + guard !pathExtension.isEmpty, !relativePath().isEmpty else { return self } var components = URLComponents(parseInfo: _parseInfo) // pathExtension might need to be percent-encoded, so use .path let newPath = components.path.appendingPathExtension(pathExtension) @@ -1844,7 +1878,7 @@ public struct URL: Equatable, Sendable, Hashable { var components = URLComponents(parseInfo: _parseInfo) let newPath = components.percentEncodedPath.removingDotSegments components.percentEncodedPath = newPath - return components.url(relativeTo: baseURL)! + return components.url(relativeTo: baseURL) ?? self } /// Standardizes the path of a file URL by removing dot segments. @@ -2060,7 +2094,7 @@ public struct URL: Equatable, Sendable, Hashable { return } #endif - if let parseInfo = Parser.parse(urlString: _url.relativeString, encodingInvalidCharacters: true) { + if let parseInfo = URL.parse(urlString: _url.relativeString) { _parseInfo = parseInfo } else { // Go to compatibility jail (allow `URL` as a dummy string container for `NSURL` instead of crashing) @@ -2218,7 +2252,7 @@ extension URL { #if !NO_FILESYSTEM baseURL = baseURL ?? .currentDirectoryOrNil() #endif - self.init(string: "", relativeTo: baseURL)! + self.init(string: "./", relativeTo: baseURL)! return } @@ -2481,6 +2515,14 @@ extension URL { #endif // NO_FILESYSTEM } #endif // FOUNDATION_FRAMEWORK + + // The old .appending(component:) implementation did not actually percent-encode + // "/" for file URLs as the documentation suggests. Many apps accidentally use + // .appending(component: "path/with/slashes") instead of using .appending(path:), + // so changing this behavior would cause breakage. + if isFileURL { + return appending(path: component, directoryHint: directoryHint, encodingSlashes: false) + } return appending(path: component, directoryHint: directoryHint, encodingSlashes: true) } diff --git a/Sources/FoundationEssentials/URL/URLComponents.swift b/Sources/FoundationEssentials/URL/URLComponents.swift index f5ce53ae7..43bd493be 100644 --- a/Sources/FoundationEssentials/URL/URLComponents.swift +++ b/Sources/FoundationEssentials/URL/URLComponents.swift @@ -142,10 +142,12 @@ public struct URLComponents: Hashable, Equatable, Sendable { return nil } - mutating func setScheme(_ newValue: String?) throws { + mutating func setScheme(_ newValue: String?, force: Bool = false) throws { reset(.scheme) - guard Parser.validate(newValue, component: .scheme) else { - throw InvalidComponentError.scheme + if !force { + guard Parser.validate(newValue, component: .scheme) else { + throw InvalidComponentError.scheme + } } _scheme = newValue if encodedHost != nil { @@ -364,6 +366,26 @@ public struct URLComponents: Hashable, Equatable, Sendable { return "" } + private var percentEncodedPathNoColon: String { + let p = percentEncodedPath + guard p.utf8.first(where: { $0 == ._colon || $0 == ._slash }) == ._colon else { + return p + } + if p.utf8.first == ._colon { + // In the rare case that an app relies on URL allowing an empty + // scheme and passes its URL string directly to URLComponents + // to modify other components, we need to return the path without + // encoding the colons. + return p + } + let firstSlash = p.utf8.firstIndex(of: ._slash) ?? p.endIndex + let colonEncodedSegment = Array(p[.. URLParseInfo? + static func parse(urlString: String, encodingInvalidCharacters: Bool, compatibility: URLParserCompatibility) -> URLParseInfo? + static func validate(_ string: (some StringProtocol)?, component: URLComponents.Component) -> Bool static func validate(_ string: (some StringProtocol)?, component: URLComponents.Component, percentEncodingAllowed: Bool) -> Bool @@ -401,15 +408,18 @@ internal struct RFC3986Parser: URLParserProtocol { } /// Fast path used during initial URL buffer parsing. - private static func validate(schemeBuffer: Slice>) -> Bool { - guard let first = schemeBuffer.first, - first >= UInt8(ascii: "A"), + private static func validate(schemeBuffer: Slice>, compatibility: URLParserCompatibility = .init()) -> Bool { + guard let first = schemeBuffer.first else { + return compatibility.contains(.allowEmptyScheme) + } + guard first >= UInt8(ascii: "A"), validate(buffer: schemeBuffer, component: .scheme, percentEncodingAllowed: false) else { return false } return true } + /// Only used by URLComponents, don't need to consider `URLParserCompatibility.allowEmptyScheme` private static func validate(scheme: some StringProtocol) -> Bool { // A valid scheme must start with an ALPHA character. // If first >= "A" and is in schemeAllowed, then first is ALPHA. @@ -593,10 +603,14 @@ internal struct RFC3986Parser: URLParserProtocol { /// Parses a URL string into `URLParseInfo`, with the option to add (or skip) encoding of invalid characters. /// If `encodingInvalidCharacters` is `true`, this function handles encoding of invalid components. static func parse(urlString: String, encodingInvalidCharacters: Bool) -> URLParseInfo? { + return parse(urlString: urlString, encodingInvalidCharacters: encodingInvalidCharacters, compatibility: .init()) + } + + static func parse(urlString: String, encodingInvalidCharacters: Bool, compatibility: URLParserCompatibility) -> URLParseInfo? { #if os(Windows) let urlString = urlString.replacing(UInt8(ascii: "\\"), with: UInt8(ascii: "/")) #endif - guard let parseInfo = parse(urlString: urlString) else { + guard let parseInfo = parse(urlString: urlString, compatibility: compatibility) else { return nil } @@ -690,10 +704,10 @@ internal struct RFC3986Parser: URLParserProtocol { /// Parses a URL string into its component parts and stores these ranges in a `URLParseInfo`. /// This function calls `parse(buffer:)`, then converts the buffer ranges into string ranges. - private static func parse(urlString: String) -> URLParseInfo? { + private static func parse(urlString: String, compatibility: URLParserCompatibility = .init()) -> URLParseInfo? { var string = urlString let bufferParseInfo = string.withUTF8 { - parse(buffer: $0) + parse(buffer: $0, compatibility: compatibility) } guard let bufferParseInfo else { return nil @@ -726,7 +740,7 @@ internal struct RFC3986Parser: URLParserProtocol { /// Parses a URL string into its component parts and stores these ranges in a `URLBufferParseInfo`. /// This function only parses based on delimiters and does not do any encoding. - private static func parse(buffer: UnsafeBufferPointer) -> URLBufferParseInfo? { + private static func parse(buffer: UnsafeBufferPointer, compatibility: URLParserCompatibility = .init()) -> URLBufferParseInfo? { // A URI is either: // 1. scheme ":" hier-part [ "?" query ] [ "#" fragment ] // 2. relative-ref @@ -746,12 +760,12 @@ internal struct RFC3986Parser: URLParserProtocol { let v = buffer[currentIndex] if v == UInt8(ascii: ":") { // Scheme must be at least 1 character, otherwise this is a relative-ref. - if currentIndex != buffer.startIndex { + if currentIndex != buffer.startIndex || compatibility.contains(.allowEmptyScheme) { parseInfo.schemeRange = buffer.startIndex.. 1) { // The trailing slash is stripped in .path for file system compatibility @@ -607,11 +607,13 @@ final class URLTests : XCTestCase { XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/no:slash") XCTAssertEqual(appended.relativePath, "relative/no:slash") - // `appending(component:)` should explicitly treat `component` as a single - // path component, meaning "/" should be encoded to "%2F" before appending + // .appending(component:) should explicitly treat slashComponent as a single + // path component, meaning "/" should be encoded to "%2F" before appending. + // However, the old behavior didn't do this for file URLs, so we maintain the + // old behavior to prevent breakage. appended = url.appending(component: slashComponent, directoryHint: .notDirectory) - 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") + XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/with:slash") + XCTAssertEqual(appended.relativePath, "relative/with:slash") appended = url.appendingPathComponent(component, isDirectory: false) XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/no:slash") @@ -687,7 +689,7 @@ final class URLTests : XCTestCase { checkBehavior(relative.path, new: "/", old: "/..") relative = URL(filePath: "", relativeTo: absolute) - checkBehavior(relative.relativePath, new: "", old: ".") + XCTAssertEqual(relative.relativePath, ".") XCTAssertTrue(relative.hasDirectoryPath) XCTAssertEqual(relative.path, "/absolute") @@ -913,6 +915,15 @@ final class URLTests : XCTestCase { 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///") + + url = URL(filePath: "/tmp/x") + url.appendPathExtension("") + XCTAssertEqual(url.path(), "/tmp/x") + XCTAssertEqual(url, url.deletingPathExtension().appendingPathExtension(url.pathExtension)) + + url = URL(filePath: "/tmp/x.") + url.deletePathExtension() + XCTAssertEqual(url.path(), "/tmp/x.") } func testURLAppendingToEmptyPath() throws { @@ -955,6 +966,21 @@ final class URLTests : XCTestCase { XCTAssertEqual(schemeOnly.absoluteString, "scheme:foo") } + func testURLEmptySchemeCompatibility() throws { + var url = try XCTUnwrap(URL(string: ":memory:")) + XCTAssertEqual(url.scheme, "") + + let base = try XCTUnwrap(URL(string: "://home")) + XCTAssertEqual(base.host(), "home") + + url = try XCTUnwrap(URL(string: "/path", relativeTo: base)) + XCTAssertEqual(url.scheme, "") + XCTAssertEqual(url.host(), "home") + XCTAssertEqual(url.path, "/path") + XCTAssertEqual(url.absoluteString, "://home/path") + XCTAssertEqual(url.absoluteURL.scheme, "") + } + func testURLComponentsPercentEncodedUnencodedProperties() throws { var comp = URLComponents() @@ -1336,6 +1362,29 @@ final class URLTests : XCTestCase { comp = try XCTUnwrap(URLComponents(string: legalURLString)) XCTAssertEqual(comp.string, legalURLString) XCTAssertEqual(comp.percentEncodedPath, colonFirstPath) + + // Colons should be percent-encoded by URLComponents.string if + // they could be misinterpreted as a scheme separator. + + comp = URLComponents() + comp.percentEncodedPath = "not%20a%20scheme:" + XCTAssertEqual(comp.string, "not%20a%20scheme%3A") + + // These would fail if we did not percent-encode the colon. + // .string should always produce a valid URL string, or nil. + + XCTAssertNotNil(URL(string: comp.string!)) + XCTAssertNotNil(URLComponents(string: comp.string!)) + + // In rare cases, an app might rely on URL allowing an empty scheme, + // but then take that string and pass it to URLComponents to modify + // other components of the URL. We shouldn't percent-encode the colon + // in these cases. + + let url = try XCTUnwrap(URL(string: "://host/path")) + comp = try XCTUnwrap(URLComponents(string: url.absoluteString)) + comp.query = "key=value" + XCTAssertEqual(comp.string, "://host/path?key=value") } func testURLComponentsInvalidPaths() { @@ -1416,6 +1465,12 @@ final class URLTests : XCTestCase { XCTAssertEqual(comp.path, "/my\u{0}path") } + func testURLStandardizedEmptyString() { + let url = URL(string: "../../../")! + let standardized = url.standardized + XCTAssertTrue(standardized.path().isEmpty) + } + #if FOUNDATION_FRAMEWORK func testURLComponentsBridging() { var nsURLComponents = NSURLComponents(