From ef651ce3a0e9e5f83db508c1ecfd85e996a4fb3d Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Tue, 3 Dec 2024 14:16:07 -0800 Subject: [PATCH 01/12] Properly write non-ASCII file names on Windows for file creation (#1060) --- .../Data/Data+Writing.swift | 22 +++++++++++-------- .../FileManager/FileManagerTests.swift | 14 ++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 7c23e1eae..3ff622d34 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -37,18 +37,22 @@ import WASILibc // 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/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index a39077d9c..3330d4195 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -1037,4 +1037,18 @@ final class FileManagerTests : XCTestCase { XCTAssertEqual(FileManager.default.homeDirectory(forUser: UUID().uuidString), fallbackPath) #endif } + + func testWindowsDirectoryCreationCrash() throws { + try FileManagerPlayground { + Directory("a\u{301}") { + + } + }.test { + XCTAssertTrue($0.fileExists(atPath: "a\u{301}")) + let data = randomData() + XCTAssertTrue($0.createFile(atPath: "a\u{301}/test", contents: data)) + XCTAssertTrue($0.fileExists(atPath: "a\u{301}/test")) + XCTAssertEqual($0.contents(atPath: "a\u{301}/test"), data) + } + } } From 80093d673e01eea40eb65760880f429fc7d9fa55 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 12 Dec 2024 09:35:40 -0800 Subject: [PATCH 02/12] Ensure that FileManager.copyItem cannot copy directory metadata to files (#1081) (#1083) * (135575520) Ensure that FileManager.copyItem cannot copy directory metadata to files * Fix whitespacing * Fix Windows test failure --- .../FileManager/FileOperations.swift | 98 ++++++++++++++++--- .../FileManager/FileManagerTests.swift | 6 +- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileOperations.swift b/Sources/FoundationEssentials/FileManager/FileOperations.swift index 6f228d808..83d131a5b 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations.swift @@ -908,6 +908,91 @@ 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 { + // 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 */ + } + } + } + } + var statInfo = stat() + if fstat(srcFD, &statInfo) == 0 { + // Copy owner/group + if fchown(dstFD, statInfo.st_uid, statInfo.st_gid) != 0 { + try delegate.throwIfNecessary(errno, srcPath(), dstPath()) + } + + // Copy modification date + let value = timeval(tv_sec: statInfo.st_mtim.tv_sec, tv_usec: statInfo.st_mtim.tv_nsec / 1000) + var tv = (value, value) + try withUnsafePointer(to: &tv) { + try $0.withMemoryRebound(to: timeval.self, capacity: 2) { + if futimes(dstFD, $0) != 0 { + try delegate.throwIfNecessary(errno, srcPath(), dstPath()) + } + } + } + + // Copy permissions + if fchmod(dstFD, statInfo.st_mode) != 0 { + try delegate.throwIfNecessary(errno, srcPath(), dstPath()) + } + } 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 +1085,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/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 3330d4195..d9bd8e59f 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -495,7 +495,7 @@ final class FileManagerTests : XCTestCase { func testCopyItemAtPathToPath() throws { let data = randomData() try FileManagerPlayground { - Directory("dir") { + Directory("dir", attributes: [.posixPermissions : 0o777]) { File("foo", contents: data) "bar" } @@ -510,8 +510,10 @@ final class FileManagerTests : XCTestCase { XCTAssertEqual($0.delegateCaptures.shouldCopy, [.init("dir", "dir2"), .init("dir/bar", "dir2/bar"), .init("dir/foo", "dir2/foo")]) #else XCTAssertEqual($0.delegateCaptures.shouldCopy, [.init("dir", "dir2"), .init("dir/foo", "dir2/foo"), .init("dir/bar", "dir2/bar")]) + + // Specifically for non-Windows (where copying directory metadata takes a special path) double check that the metadata was copied exactly + XCTAssertEqual(try $0.attributesOfItem(atPath: "dir2")[.posixPermissions] as? UInt, 0o777) #endif - XCTAssertThrowsError(try $0.copyItem(atPath: "does_not_exist", toPath: "dir3")) { XCTAssertEqual(($0 as? CocoaError)?.code, .fileReadNoSuchFile) } From 3a3d5dc82d13e2d482110b8e5da5f68ef0fe45ce Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 12 Dec 2024 09:36:04 -0800 Subject: [PATCH 03/12] (140882573) Home directory for non-existent user should not fall back to /var/empty or %ALLUSERSPROFILE% (#1072) (#1073) --- .../FileManager/FileManager+Directories.swift | 8 +- .../String/String+Path.swift | 171 ++++++++++-------- .../FileManager/FileManagerTests.swift | 10 +- 3 files changed, 101 insertions(+), 88 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift index 7d74d8636..9efd5523c 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift @@ -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/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index a2ebfe826..8fb1d8aa2 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -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/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index d9bd8e59f..8236b1471 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -1029,14 +1029,8 @@ final class FileManagerTests : XCTestCase { #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) + XCTAssertNil(FileManager.default.homeDirectory(forUser: "")) + XCTAssertNil(FileManager.default.homeDirectory(forUser: UUID().uuidString)) #endif } From ae38607764abba7d41b40b8f6fb478166812f8e0 Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:33:06 -0700 Subject: [PATCH 04/12] (141294361) URL.appendingPathExtension("") appends a trailing dot (#1082) (#1091) --- Sources/FoundationEssentials/String/String+Path.swift | 2 +- Sources/FoundationEssentials/URL/URL.swift | 2 +- Tests/FoundationEssentialsTests/URLTests.swift | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 8fb1d8aa2..bec7df2c4 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -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 diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index 27ca1b969..294024f40 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -1754,7 +1754,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) diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 0b7a3e649..0e2cb3517 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -913,6 +913,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 { From ca7070fdd39fd828200a406fb23f76970067122b Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 2 Jan 2025 09:18:03 -0800 Subject: [PATCH 05/12] Implement LockedState for Musl (#1101) (#1104) --- Sources/FoundationEssentials/LockedState.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/FoundationEssentials/LockedState.swift b/Sources/FoundationEssentials/LockedState.swift index ac7539f1b..0cca43328 100644 --- a/Sources/FoundationEssentials/LockedState.swift +++ b/Sources/FoundationEssentials/LockedState.swift @@ -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 } } From bca2f46d080d2d79332143d36df44350e63c6770 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 4 Jan 2025 07:39:59 +0900 Subject: [PATCH 06/12] [6.1] Fix WASI build of `_copyDirectoryMetadata` (#1099) * Fix WASI build of `_copyDirectoryMetadata` (#1094) Extended attributes don't exist in WASI, so we need to exclude the use of xattr-related APIs including `flistxattr`. * Follow-up fixes to make it work with wasi-libc (#1095) * Gate `fchown` and `fchmod` calls behind `os(WASI)` They are not available on WASI, so we gate them behind `os(WASI)`. * Add missing constant shims for wasi-libc * Use `futimens` instead of legacy `futimes` wasi-libc does not provide `futimes` as it is a legacy function. https://github.com/WebAssembly/wasi-libc/blob/574b88da481569b65a237cb80daf9a2d5aeaf82d/libc-top-half/musl/include/sys/time.h#L34 * Don't try to set extended attributes on Android (#1106) Normal users don't have permission to change these, even for their own files. --------- Co-authored-by: finagolfin --- .../FileManager/FileOperations.swift | 12 +++++++++--- .../FoundationEssentials/WASILibc+Extensions.swift | 9 +++++++++ Sources/_FoundationCShims/include/platform_shims.h | 4 ++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileOperations.swift b/Sources/FoundationEssentials/FileManager/FileOperations.swift index 83d131a5b..b29e7121a 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations.swift @@ -911,6 +911,7 @@ enum _FileOperations { #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 { @@ -936,28 +937,33 @@ enum _FileOperations { } } } + #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 = timeval(tv_sec: statInfo.st_mtim.tv_sec, tv_usec: statInfo.st_mtim.tv_nsec / 1000) + let value = statInfo.st_mtim var tv = (value, value) try withUnsafePointer(to: &tv) { - try $0.withMemoryRebound(to: timeval.self, capacity: 2) { - if futimes(dstFD, $0) != 0 { + 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, statInfo.st_mode) != 0 { try delegate.throwIfNecessary(errno, srcPath(), dstPath()) } + #endif } else { try delegate.throwIfNecessary(errno, srcPath(), dstPath()) } diff --git a/Sources/FoundationEssentials/WASILibc+Extensions.swift b/Sources/FoundationEssentials/WASILibc+Extensions.swift index 351fe19f2..44f3f936a 100644 --- a/Sources/FoundationEssentials/WASILibc+Extensions.swift +++ b/Sources/FoundationEssentials/WASILibc+Extensions.swift @@ -49,5 +49,14 @@ internal var O_TRUNC: Int32 { internal var O_WRONLY: Int32 { return _platform_shims_O_WRONLY() } +internal var O_RDONLY: Int32 { + return _platform_shims_O_RDONLY() +} +internal var O_DIRECTORY: Int32 { + return _platform_shims_O_DIRECTORY() +} +internal var O_NOFOLLOW: Int32 { + return _platform_shims_O_NOFOLLOW() +} #endif // os(WASI) diff --git a/Sources/_FoundationCShims/include/platform_shims.h b/Sources/_FoundationCShims/include/platform_shims.h index 6bc0a0e15..e02b58177 100644 --- a/Sources/_FoundationCShims/include/platform_shims.h +++ b/Sources/_FoundationCShims/include/platform_shims.h @@ -102,6 +102,10 @@ static inline int32_t _platform_shims_O_CREAT(void) { return O_CREAT; } static inline int32_t _platform_shims_O_EXCL(void) { return O_EXCL; } static inline int32_t _platform_shims_O_TRUNC(void) { return O_TRUNC; } static inline int32_t _platform_shims_O_WRONLY(void) { return O_WRONLY; } +static inline int32_t _platform_shims_O_RDONLY(void) { return O_RDONLY; } +static inline int32_t _platform_shims_O_DIRECTORY(void) { return O_DIRECTORY; } +static inline int32_t _platform_shims_O_NOFOLLOW(void) { return O_NOFOLLOW; } + #endif #endif /* CSHIMS_PLATFORM_SHIMS */ From 43fbf1ef5512e70fb7d5335c9bf849ebf5399fe0 Mon Sep 17 00:00:00 2001 From: finagolfin Date: Fri, 17 Jan 2025 11:46:39 +0530 Subject: [PATCH 07/12] [android] fix 32-bit build (#1086) (#1120) Regression after bb3fccfa360d00f63999ac8faf6ba37224ce5174 Co-authored-by: Alex Lorenz --- Sources/FoundationEssentials/FileManager/FileOperations.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/FileManager/FileOperations.swift b/Sources/FoundationEssentials/FileManager/FileOperations.swift index b29e7121a..38baabbb0 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations.swift @@ -960,7 +960,7 @@ enum _FileOperations { #if !os(WASI) // WASI doesn't have fchmod for now // Copy permissions - if fchmod(dstFD, statInfo.st_mode) != 0 { + if fchmod(dstFD, mode_t(statInfo.st_mode)) != 0 { try delegate.throwIfNecessary(errno, srcPath(), dstPath()) } #endif From 315857493c5757440eb48539d7853bfbcbfe7afa Mon Sep 17 00:00:00 2001 From: Jeremy Day Date: Wed, 29 Jan 2025 15:11:18 -0800 Subject: [PATCH 08/12] Fix Windows .alwaysMapped deallocator (#1147) (#1148) (cherry picked from commit 281db8e767eeeb679510d81b06c8d1778f91b66e) --- Sources/FoundationEssentials/Data/Data+Reading.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationEssentials/Data/Data+Reading.swift b/Sources/FoundationEssentials/Data/Data+Reading.swift index e2ddae243..797069d93 100644 --- a/Sources/FoundationEssentials/Data/Data+Reading.swift +++ b/Sources/FoundationEssentials/Data/Data+Reading.swift @@ -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 { From 6571b345475fe08df9d84cad5830890d30191d03 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Mon, 24 Feb 2025 13:27:25 -0800 Subject: [PATCH 09/12] [6.1] Update SwiftPM/CMake dependencies to match toolchain build (#1181) --- Benchmarks/Package.swift | 4 ++-- CMakeLists.txt | 2 +- Package.swift | 4 ++-- Sources/FoundationMacros/CMakeLists.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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/FoundationMacros/CMakeLists.txt b/Sources/FoundationMacros/CMakeLists.txt index 98b90c0df..f6ae52387 100644 --- a/Sources/FoundationMacros/CMakeLists.txt +++ b/Sources/FoundationMacros/CMakeLists.txt @@ -43,7 +43,7 @@ if(NOT SwiftSyntax_FOUND) # 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 600.0.0) + GIT_TAG release/6.1) FetchContent_MakeAvailable(SwiftSyntax) else() message(STATUS "SwiftSyntax_DIR provided, using swift-syntax from ${SwiftSyntax_DIR}") From 7a8547ce241ee0a00809025390d95e2895c01b8d Mon Sep 17 00:00:00 2001 From: Johannes Weiss Date: Wed, 26 Feb 2025 22:10:48 +0000 Subject: [PATCH 10/12] always @preconcurrency import Glibc/Musl/Android/Bionic/WASILibc [6.1] (#1194) --- Sources/FoundationEssentials/Calendar/Calendar.swift | 8 ++++---- .../Calendar/Calendar_Gregorian.swift | 8 ++++---- Sources/FoundationEssentials/CodableUtilities.swift | 4 ++-- Sources/FoundationEssentials/Data/Data+Reading.swift | 8 ++++---- Sources/FoundationEssentials/Data/Data+Writing.swift | 8 ++++---- Sources/FoundationEssentials/Data/Data.swift | 8 ++++---- Sources/FoundationEssentials/Data/DataProtocol.swift | 6 +++--- Sources/FoundationEssentials/Date.swift | 8 ++++---- Sources/FoundationEssentials/Decimal/Decimal+Math.swift | 8 ++++---- Sources/FoundationEssentials/Decimal/Decimal.swift | 2 +- .../FoundationEssentials/Error/CocoaError+FilePath.swift | 8 ++++---- Sources/FoundationEssentials/Error/ErrorCodes+POSIX.swift | 2 +- .../FileManager/FileManager+Basics.swift | 8 ++++---- .../FileManager/FileManager+Directories.swift | 8 ++++---- .../FileManager/FileManager+Files.swift | 8 ++++---- .../FileManager/FileManager+SymbolicLinks.swift | 8 ++++---- .../FileManager/FileManager+Utilities.swift | 8 ++++---- .../FileManager/FileOperations+Enumeration.swift | 8 ++++---- .../FoundationEssentials/FileManager/FileOperations.swift | 8 ++++---- .../BinaryInteger+NumericStringRepresentation.swift | 8 ++++---- Sources/FoundationEssentials/JSON/JSON5Scanner.swift | 2 +- Sources/FoundationEssentials/JSON/JSONDecoder.swift | 2 +- Sources/FoundationEssentials/JSON/JSONScanner.swift | 2 +- .../FoundationEssentials/Locale/Locale+Components.swift | 2 +- Sources/FoundationEssentials/LockedState.swift | 6 +++--- Sources/FoundationEssentials/Platform.swift | 6 +++--- .../FoundationEssentials/ProcessInfo/ProcessInfo.swift | 8 ++++---- .../FoundationEssentials/PropertyList/OpenStepPlist.swift | 8 ++++---- Sources/FoundationEssentials/String/String+Path.swift | 8 ++++---- .../String/StringProtocol+Essentials.swift | 2 +- Sources/FoundationEssentials/TimeZone/TimeZone.swift | 2 +- .../FoundationEssentials/TimeZone/TimeZone_Cache.swift | 4 ++-- Sources/FoundationEssentials/WASILibc+Extensions.swift | 2 +- Sources/FoundationEssentials/_ThreadLocal.swift | 6 +++--- .../Calendar/Calendar_ICU.swift | 8 ++++---- Sources/FoundationInternationalization/Date+ICU.swift | 6 +++--- .../Formatting/Date/ICUDateFormatter.swift | 6 +++--- .../Formatting/Duration+Formatting.swift | 8 ++++---- .../Locale/Locale+Components_ICU.swift | 2 +- .../Locale/Locale_ICU.swift | 2 +- .../TimeZone/TimeZone_ICU.swift | 2 +- 41 files changed, 118 insertions(+), 118 deletions(-) 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 797069d93..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 { diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 3ff622d34..347757b42 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -20,17 +20,17 @@ 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 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 9efd5523c..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 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 38baabbb0..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 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 0cca43328..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 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+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index bec7df2c4..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 diff --git a/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift b/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift index c86c6e664..003fc486a 100644 --- a/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift +++ b/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift @@ -16,7 +16,7 @@ internal import _ForSwiftFoundation #if canImport(Darwin) import Darwin #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif internal import _FoundationCShims 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/WASILibc+Extensions.swift b/Sources/FoundationEssentials/WASILibc+Extensions.swift index 44f3f936a..529ac7731 100644 --- a/Sources/FoundationEssentials/WASILibc+Extensions.swift +++ b/Sources/FoundationEssentials/WASILibc+Extensions.swift @@ -11,7 +11,7 @@ #if os(WASI) -import WASILibc +@preconcurrency import WASILibc internal import _FoundationCShims // MARK: - Clock diff --git a/Sources/FoundationEssentials/_ThreadLocal.swift b/Sources/FoundationEssentials/_ThreadLocal.swift index 15afc6c17..c5110a432 100644 --- a/Sources/FoundationEssentials/_ThreadLocal.swift +++ b/Sources/FoundationEssentials/_ThreadLocal.swift @@ -12,11 +12,11 @@ #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 canImport(threads_h) diff --git a/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift b/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift index 87089e0c2..1e63775e0 100644 --- a/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift +++ b/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift @@ -15,17 +15,17 @@ import FoundationEssentials #endif #if canImport(Android) -import Android +@preconcurrency import Android #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif canImport(CRT) import CRT #elseif canImport(Darwin) import Darwin #elseif os(WASI) -import WASILibc +@preconcurrency import WASILibc #endif internal import _FoundationICU diff --git a/Sources/FoundationInternationalization/Date+ICU.swift b/Sources/FoundationInternationalization/Date+ICU.swift index 4da356764..3bacc322b 100644 --- a/Sources/FoundationInternationalization/Date+ICU.swift +++ b/Sources/FoundationInternationalization/Date+ICU.swift @@ -16,11 +16,11 @@ import FoundationEssentials internal import _FoundationICU #if 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(Darwin) import Darwin #endif diff --git a/Sources/FoundationInternationalization/Formatting/Date/ICUDateFormatter.swift b/Sources/FoundationInternationalization/Formatting/Date/ICUDateFormatter.swift index 6d9a08dea..ec5a1ea2c 100644 --- a/Sources/FoundationInternationalization/Formatting/Date/ICUDateFormatter.swift +++ b/Sources/FoundationInternationalization/Formatting/Date/ICUDateFormatter.swift @@ -19,11 +19,11 @@ internal import _FoundationICU #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 #endif typealias UChar = UInt16 diff --git a/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift b/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift index 8e2fe6a3c..cf2ad5b6c 100644 --- a/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift +++ b/Sources/FoundationInternationalization/Formatting/Duration+Formatting.swift @@ -17,15 +17,15 @@ import FoundationEssentials #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 @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) diff --git a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift index 5bb092e82..4fb3bd075 100644 --- a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift +++ b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// #if canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif #if canImport(FoundationEssentials) diff --git a/Sources/FoundationInternationalization/Locale/Locale_ICU.swift b/Sources/FoundationInternationalization/Locale/Locale_ICU.swift index 745459a60..c22025ee8 100644 --- a/Sources/FoundationInternationalization/Locale/Locale_ICU.swift +++ b/Sources/FoundationInternationalization/Locale/Locale_ICU.swift @@ -24,7 +24,7 @@ internal import os internal import _FoundationICU #if canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif #if !FOUNDATION_FRAMEWORK diff --git a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift index bff665f82..f1b35e649 100644 --- a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift +++ b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift @@ -15,7 +15,7 @@ import FoundationEssentials #endif #if canImport(Glibc) -import Glibc +@preconcurrency import Glibc #endif #if canImport(ucrt) From 35b58c480fb2d50ea92d0cc87a7d69cf7799bbb0 Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:59:41 -0800 Subject: [PATCH 11/12] [6.1] URL compatibility and bug fixes (#1200) * (141549683) Restore behavior of URL(string: "") returning nil (#1103) * (142076445) Allow URL.standardized to return an empty string URL (#1110) * (142076445) Allow URL.standardized to return an empty string URL * Add ?? self to prevent force-unwrap * (142446243) Compatibility behaviors for Swift URL (#1113) * (142589056) URLComponents.string should percent-encode colons in first path segment if needed (#1117) * (142667792) URL.absoluteString crashes if baseURL starts with colon (#1119) * (143159003) Don't encode colon if URLComponents path starts with colon (#1139) --- Sources/FoundationEssentials/URL/URL.swift | 102 ++++++++++++------ .../URL/URLComponents.swift | 45 ++++++-- .../FoundationEssentials/URL/URLParser.swift | 34 ++++-- .../FoundationEssentialsTests/URLTests.swift | 60 +++++++++-- 4 files changed, 188 insertions(+), 53 deletions(-) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index 294024f40..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) @@ -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") @@ -964,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() @@ -1345,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() { @@ -1425,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( From 7d4817bc3786f8b1f0a2f91b11a3d126d9283b15 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 20 Mar 2025 18:33:14 -0700 Subject: [PATCH 12/12] Fix ISO Latin 1 Encoding/Decoding issues (#1219) (#1221) --- .../String/String+IO.swift | 36 ++----------------- .../String/StringProtocol+Essentials.swift | 16 ++++----- .../StringTests.swift | 4 ++- 3 files changed, 12 insertions(+), 44 deletions(-) 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/StringProtocol+Essentials.swift b/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift index 003fc486a..4da60c1a6 100644 --- a/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift +++ b/Sources/FoundationEssentials/String/StringProtocol+Essentials.swift @@ -21,12 +21,6 @@ import Darwin 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/Tests/FoundationEssentialsTests/StringTests.swift b/Tests/FoundationEssentialsTests/StringTests.swift index 1daa98b92..81cfa1954 100644 --- a/Tests/FoundationEssentialsTests/StringTests.swift +++ b/Tests/FoundationEssentialsTests/StringTests.swift @@ -1336,7 +1336,9 @@ final class StringTests : XCTestCase { "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789", "!\"#$%&'()*+,-./", - "¡¶ÅÖæöÿ\u{00A0}~" + "¡¶ÅÖæöÿ\u{0080}\u{00A0}~", + "Hello\nworld", + "Hello\r\nworld" ], invalid: [ "🎺", "מ",