diff --git a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift index fc5ef7e55..14e2c7289 100644 --- a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift +++ b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift @@ -67,6 +67,15 @@ let benchmarks = { return box }) + Benchmark("DataIdenticalEmpty", closure: { benchmark, box in + blackHole(box.d1.isIdentical(to: box.d2)) + }, setup: { () -> TwoDatasBox in + let d1 = Data() + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + Benchmark("DataEqualInline", closure: { benchmark, box in blackHole(box.d1 == box.d2) }, setup: { () -> TwoDatasBox in @@ -75,6 +84,15 @@ let benchmarks = { let box = TwoDatasBox(d1: d1, d2: d2) return box }) + + Benchmark("DataIdenticalInline", closure: { benchmark, box in + blackHole(box.d1.isIdentical(to: box.d2)) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(12) // Less than size of InlineData.Buffer + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) Benchmark("DataNotEqualInline", closure: { benchmark, box in blackHole(box.d1 != box.d2) @@ -93,7 +111,16 @@ let benchmarks = { let box = TwoDatasBox(d1: d1, d2: d2) return box }) - + + Benchmark("DataIdenticalLarge", closure: { benchmark, box in + blackHole(box.d1.isIdentical(to: box.d2)) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 8) + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + Benchmark("DataNotEqualLarge", closure: { benchmark, box in blackHole(box.d1 != box.d2) }, setup: { () -> TwoDatasBox in @@ -112,6 +139,15 @@ let benchmarks = { return box }) + Benchmark("DataIdenticalReallyLarge", closure: { benchmark, box in + blackHole(box.d1.isIdentical(to: box.d2)) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 1024 * 8) + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + Benchmark("DataNotEqualReallyLarge", closure: { benchmark, box in blackHole(box.d1 != box.d2) }, setup: { () -> TwoDatasBox in diff --git a/Proposals/0023-progress-reporter.md b/Proposals/0023-progress-reporter.md index 05dfbcfa9..c41810f60 100644 --- a/Proposals/0023-progress-reporter.md +++ b/Proposals/0023-progress-reporter.md @@ -347,7 +347,7 @@ overall.assign(count: 3, to: examCountdown.progressReporter) // Add `ProgressReporter` to another parent `ProgressManager` with different assigned count let deadlineTracker = ProgressManager(totalCount: 2) -overall.assign(count: 1, to: examCountdown, progressReporter) +deadlineTracker.assign(count: 1, to: examCountdown.progressReporter) ``` ### Reporting Progress With Type-Safe Custom Properties diff --git a/Proposals/0030-base64-urlencoding-and-omitting-padding.md b/Proposals/0030-base64-urlencoding-and-omitting-padding.md new file mode 100644 index 000000000..2c18a29fe --- /dev/null +++ b/Proposals/0030-base64-urlencoding-and-omitting-padding.md @@ -0,0 +1,82 @@ +# Adding base64 urlencoding and omitting padding option to base64 encoding and decoding + +* Proposal: [SF-0030](0030-base64-urlencoding-and-omitting-padding.md) +* Authors: [Fabian Fett](https://github.com/fabianfett) +* Review Manager: TBD +* Status: **Accepted** +* Implementation: [apple/swift-foundation#1195](https://github.com/swiftlang/swift-foundation/pull/1195) [apple/swift-foundation#1196](https://github.com/swiftlang/swift-foundation/pull/1196) +* Review: [pitch](https://forums.swift.org/t/pitch-adding-base64-urlencoding-and-omitting-padding-options-to-base64-encoding-and-decoding/77659) + +## Revision history + +* **v1** Initial version + +## Introduction + +Introducing base64 encoding and decoding options to support the base64url alphabet as defined in [RFC4648] and to allow the omission of padding characters. + +## Motivation + +Foundation offers APIs to encode data in the base64 format and to decode base64 encoded data. Multiple RFCs that define cryptography for the web use the base64url encoding and strip the padding characters in the end. Examples for this are: + +- [RFC7519 - JSON Web Token (JWT)][RFC7519] +- [RFC8291 - Message Encryption for Web Push][RFC8291] + +Since Foundation is not offering an API to support the base64url alphabet and omitting the padding characters, users create wrappers around the existing APIs using `replacingOccurrences(of:, with:)`. While this approach works it is very inefficient. The data has to be iterated three times, where one time could have been sufficient. + +## Solution + +We propose to add additional options to `Data.Base64EncodingOptions`: + +```swift +extension Data.Base64EncodingOptions { + /// Use the base64url alphabet to encode the data + @available(FoundationPreview 6.3, *) + public static var base64URLAlphabet: Base64EncodingOptions { get } + + /// Omit the `=` padding characters in the end of the base64 encoded result + @available(FoundationPreview 6.3, *) + public static var omitPaddingCharacter: Base64EncodingOptions { get } +} +``` + +Simultaneously we will add the same options to `Data.Base64DecodingOptions` and an additional `ignoreWhitespaceCharacters` option. Please note that we show the existing `ignoreUnknownCharacters` option in the code snippet below, as we intend to change its documentation to better explains its tradeoffs. + +```swift +extension Data.Base64DecodingOptions { + /// Modify the decoding algorithm so that it ignores unknown non-Base-64 bytes, including line ending characters. + /// + /// - Warning: Using `ignoreUnknownCharacters` might allow the decoding of base64url data, even when the + /// `base64URLAlphabet` is not selected. It might also allow using the base64 alphabet when the + /// `base64URLAlphabet` is selected. + /// Consider using the `ignoreWhitespaceCharacters` option if possible. + public static let ignoreUnknownCharacters = Base64DecodingOptions(rawValue: 1 << 0) + + /// Modify the decoding algorithm so that it ignores whitespace characters (CR LF Tab and Space). + /// + /// The decoding will fail if any other invalid character is found in the encoded data. + @available(FoundationPreview 6.3, *) + public static var ignoreWhitespaceCharacters: Base64DecodingOptions { get } + + /// Modify the decoding algorithm so that it expects base64 encoded data that uses base64url alphabet. + @available(FoundationPreview 6.3, *) + public static var base64URLAlphabet: Base64EncodingOptions { get } + + /// Modify the decoding algorithm so that it expects no padding characters at the end of the encoded data. + /// + /// The decoding will fail if the padding character `=` is used at the end of the encoded data. + /// + /// - Warning: This option is ignored if `ignoreUnknownCharacters` is used at the same time. Consider + /// using `ignoreWhitespaceCharacters` if possible. + @available(FoundationPreview 6.3, *) + public static var omitPaddingCharacter: Base64EncodingOptions { get } +} +``` + +## Impact on existing code + +None. This is an additive change. + +[RFC4648]: https://datatracker.ietf.org/doc/html/rfc4648 +[RFC7519]: https://datatracker.ietf.org/doc/html/rfc7519 +[RFC8291]: https://datatracker.ietf.org/doc/html/rfc8291 diff --git a/Proposals/NNNN-random-uuid.md b/Proposals/0031-random-uuid.md similarity index 91% rename from Proposals/NNNN-random-uuid.md rename to Proposals/0031-random-uuid.md index 8410ee13c..d59fbaf2f 100644 --- a/Proposals/NNNN-random-uuid.md +++ b/Proposals/0031-random-uuid.md @@ -1,11 +1,11 @@ # Generating UUIDs using RandomNumberGenerators -* Proposal: [SF-NNNN](NNNN-random-uuid.md) +* Proposal: [SF-0031](0031-random-uuid.md) * Authors: [FranzBusch](https://github.com/FranzBusch) * Review Manager: TBD -* Status: **Awaiting review** +* Status: **Accepted & Implemented** * Implementation: [swiftlang/swift-foundation#1271](https://github.com/swiftlang/swift-foundation/pull/1271) -* Review: ([pitch](https://forums.swift.org/...)) +* Review: ([pitch/review](https://forums.swift.org/t/review-generating-uuids-using-randomnumbergenerators/79494/19)) ## Introduction diff --git a/Sources/FoundationEssentials/AttributedString/AttributeScope.swift b/Sources/FoundationEssentials/AttributedString/AttributeScope.swift index 2fac17426..d2c47b18d 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributeScope.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributeScope.swift @@ -131,7 +131,7 @@ internal func _loadDefaultAttributes() -> [String : any AttributedStringKey.Type // UIKit on macOS let macUIScope = ( "$s10Foundation15AttributeScopesO5UIKitE0D10AttributesVN", - "/System/iOSSupport/System/Library/Frameworks/UIKit.framework/UIKit" + "/System/iOSSupport/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore" ) #endif @@ -142,12 +142,12 @@ internal func _loadDefaultAttributes() -> [String : any AttributedStringKey.Type // UIKit ( "$s10Foundation15AttributeScopesO5UIKitE0D10AttributesVN", - "/System/Library/Frameworks/UIKit.framework/UIKit" + "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore" ), // SwiftUI ( "$s10Foundation15AttributeScopesO7SwiftUIE0D12UIAttributesVN", - "/System/Library/Frameworks/SwiftUI.framework/SwiftUI" + "/System/Library/Frameworks/SwiftUICore.framework/SwiftUICore" ), // Accessibility ( diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 51b54ed5e..6f51c3d16 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -157,9 +157,37 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL, let pidString = String(ProcessInfo.processInfo.processIdentifier, radix: 16, uppercase: true) let template = directoryPath + prefix + pidString + ".XXXXXX" - var count = 0 let maxCount = 7 - repeat { + for _ in 0 ..< maxCount { +#if FOUNDATION_FRAMEWORK + let (sandboxResult, amkrErrno) = inPath.withFileSystemRepresentation { inPathFileSystemRep -> ((Int32, String)?, Int32?) in + guard let inPathFileSystemRep else { + return (nil, nil) + } + // First, try _amkrtemp to carry over any sandbox extensions for inPath to the temporary file (even if the application isn't sandboxed) + guard let uniqueTempFile = _amkrtemp(inPathFileSystemRep) else { + return (nil, errno) + } + defer { free(uniqueTempFile) } + let fd = openFileDescriptorProtected(path: uniqueTempFile, flags: O_CREAT | O_EXCL | O_RDWR, options: options) + if fd >= 0 { + // Got a good fd + return ((fd, String(cString: uniqueTempFile)), nil) + } + return (nil, errno) + } + + // If _amkrtemp succeeded, return its result + if let sandboxResult { + return sandboxResult + } + // If _amkrtemp failed with EEXIST, just retry + if amkrErrno == EEXIST { + continue + } + // Otherwise, fall through to mktemp below +#endif + let result = try template.withMutableFileSystemRepresentation { templateFileSystemRep -> (Int32, String)? in guard let templateFileSystemRep else { throw CocoaError(.fileWriteInvalidFileName) @@ -187,7 +215,12 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL, // If the file exists, we repeat. Otherwise throw the error. if errno != EEXIST { - throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant) + #if FOUNDATION_FRAMEWORK + let debugDescription = "Creating a temporary file via mktemp failed. Creating the temporary file via _amkrtemp previously also failed with errno \(amkrErrno)" + #else + let debugDescription: String? = nil + #endif + throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant, debugDescription: debugDescription) } // Try again @@ -196,14 +229,10 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL, if let result { return result - } else { - count += 1 - if count > maxCount { - // Prevent an infinite loop; even if the error is obscure - throw CocoaError(.fileWriteUnknown) - } } - } while true + } + // We hit max count, prevent an infinite loop; even if the error is obscure + throw CocoaError(.fileWriteUnknown) #endif // os(WASI) } @@ -358,44 +387,88 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint // TODO: Somehow avoid copying back and forth to a String to hold the path #if os(Windows) - try inPath.path.withNTPathRepresentation { pwszPath in - var (fd, auxPath, temporaryDirectoryPath) = try createProtectedTemporaryFile(at: inPath.path, inPath: inPath, options: options, variant: "Folder") + var (fd, auxPath, temporaryDirectoryPath) = try createProtectedTemporaryFile(at: inPath.path, inPath: inPath, options: options, variant: "Folder") - // Cleanup temporary directory - defer { cleanupTemporaryDirectory(at: temporaryDirectoryPath) } + // Cleanup temporary directory + defer { cleanupTemporaryDirectory(at: temporaryDirectoryPath) } - guard fd >= 0 else { + guard fd >= 0 else { + throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false) + } + + defer { if fd >= 0 { _close(fd) } } + + let callback = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.count)) : nil + + do { + try write(buffer: buffer, toFileDescriptor: fd, path: inPath, parentProgress: callback) + } catch { + try auxPath.withNTPathRepresentation { pwszAuxPath in + _ = DeleteFileW(pwszAuxPath) + } + + if callback?.isCancelled ?? false { + throw CocoaError(.userCancelled) + } else { throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false) } + } - defer { if fd >= 0 { _close(fd) } } + writeExtendedAttributes(fd: fd, attributes: attributes) - let callback = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.count)) : nil + _close(fd) + fd = -1 - do { - try write(buffer: buffer, toFileDescriptor: fd, path: inPath, parentProgress: callback) - } catch { - try auxPath.withNTPathRepresentation { pwszAuxPath in - _ = DeleteFileW(pwszAuxPath) - } + try auxPath.withNTPathRepresentation { pwszAuxiliaryPath in + defer { _ = DeleteFileW(pwszAuxiliaryPath) } - if callback?.isCancelled ?? false { - throw CocoaError(.userCancelled) - } else { - throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false) + var hFile = CreateFileW(pwszAuxiliaryPath, DELETE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nil, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + nil) + if hFile == INVALID_HANDLE_VALUE { + throw CocoaError.errorWithFilePath(inPath, win32: GetLastError(), reading: false) + } + + defer { + switch hFile { + case INVALID_HANDLE_VALUE: + break + default: + _ = CloseHandle(hFile) } } - writeExtendedAttributes(fd: fd, attributes: attributes) + try inPath.path.withNTPathRepresentation { pwszPath in + let cchLength = wcslen(pwszPath) + let cbSize = cchLength * MemoryLayout.size + let dwSize = DWORD(MemoryLayout.size + cbSize + MemoryLayout.size) + try withUnsafeTemporaryAllocation(byteCount: Int(dwSize), + alignment: MemoryLayout.alignment) { pBuffer in + var pInfo = pBuffer.baseAddress?.bindMemory(to: FILE_RENAME_INFO.self, capacity: 1) + pInfo?.pointee.Flags = FILE_RENAME_FLAG_POSIX_SEMANTICS | FILE_RENAME_FLAG_REPLACE_IF_EXISTS + pInfo?.pointee.RootDirectory = nil + pInfo?.pointee.FileNameLength = DWORD(cbSize) + pBuffer.baseAddress?.advanced(by: MemoryLayout.offset(of: \.FileName)!) + .withMemoryRebound(to: WCHAR.self, capacity: cchLength + 1) { + wcscpy_s($0, cchLength + 1, pwszPath) + } + + if !SetFileInformationByHandle(hFile, FileRenameInfoEx, pInfo, dwSize) { + let dwError = GetLastError() + guard dwError == ERROR_NOT_SAME_DEVICE else { + throw CocoaError.errorWithFilePath(inPath, win32: dwError, reading: false) + } - _close(fd) - fd = -1 + _ = CloseHandle(hFile) + hFile = INVALID_HANDLE_VALUE - try auxPath.withNTPathRepresentation { pwszAuxiliaryPath in - guard MoveFileExW(pwszAuxiliaryPath, pwszPath, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) else { - let dwError = GetLastError() - _ = DeleteFileW(pwszAuxiliaryPath) - throw CocoaError.errorWithFilePath(inPath, win32: dwError, reading: false) + // The move is across volumes. + guard MoveFileExW(pwszAuxiliaryPath, pwszPath, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING) else { + throw CocoaError.errorWithFilePath(inPath, win32: GetLastError(), reading: false) + } + } } } } diff --git a/Sources/FoundationEssentials/Data/Data.swift b/Sources/FoundationEssentials/Data/Data.swift index ecea12041..cb96088cc 100644 --- a/Sources/FoundationEssentials/Data/Data.swift +++ b/Sources/FoundationEssentials/Data/Data.swift @@ -2974,3 +2974,53 @@ extension Data : Codable { } } } + +extension Data { + /// Returns a boolean value indicating whether this data is identical to + /// `other`. + /// + /// Two data values are identical if there is no way to distinguish between + /// them. + /// + /// Comparing data this way includes comparing (normally) hidden + /// implementation details such as the memory location of any underlying + /// data storage object. Therefore, identical data are guaranteed to + /// compare equal with `==`, but not all equal data are considered + /// identical. + /// + /// - Performance: O(1) + @_alwaysEmitIntoClient + public func isIdentical(to other: Self) -> Bool { + // See if both are empty + switch (self._representation, other._representation) { + case (.empty, .empty): + return true + case (.inline, .inline), (.slice, .slice), (.large, .large): + // Continue on to checks below + break + default: + return false + } + + let length1 = self.count + let length2 = other.count + + // Unequal length data can never be equal + guard length1 == length2 else { + return false + } + + if length1 > 0 { + return self.withUnsafeBytes { (b1: UnsafeRawBufferPointer) in + return other.withUnsafeBytes { (b2: UnsafeRawBufferPointer) in + // If they have the same base address and same count, it is equal + let b1Address = b1.baseAddress! + let b2Address = b2.baseAddress! + + return b1Address == b2Address + } + } + } + return true + } +} diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift b/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift index 26c0406e8..eabde824b 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift @@ -80,7 +80,6 @@ extension Decimal : _ObjectiveCBridgeable { // MARK: - Bridging code to C functions // We have one implementation function for each, and an entry point for both Darwin (cdecl, exported from the framework), and swift-corelibs-foundation (SPI here and available via that package as API) -#if FOUNDATION_FRAMEWORK @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) public func pow(_ x: Decimal, _ y: Int) -> Decimal { let result = try? x._power( @@ -88,15 +87,6 @@ public func pow(_ x: Decimal, _ y: Int) -> Decimal { ) return result ?? .nan } -#else -@_spi(SwiftCorelibsFoundation) -public func _pow(_ x: Decimal, _ y: Int) -> Decimal { - let result = try? x._power( - exponent: y, roundingMode: .plain - ) - return result ?? .nan -} -#endif private func __NSDecimalAdd( _ result: UnsafeMutablePointer, diff --git a/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift b/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift index 5dc64f876..b28aa6323 100644 --- a/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift +++ b/Sources/FoundationEssentials/Error/CocoaError+FilePath.swift @@ -31,12 +31,12 @@ import WinSDK // MARK: - Error Creation with CocoaError.Code extension CocoaError { - static func errorWithFilePath(_ code: CocoaError.Code, _ path: String, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError { - CocoaError(code, path: path, variant: variant, source: source, destination: destination) + static func errorWithFilePath(_ code: CocoaError.Code, _ path: String, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError { + CocoaError(code, path: path, variant: variant, source: source, destination: destination, debugDescription: debugDescription) } - static func errorWithFilePath(_ code: CocoaError.Code, _ url: URL, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError { - CocoaError(code, url: url, variant: variant, source: source, destination: destination) + static func errorWithFilePath(_ code: CocoaError.Code, _ url: URL, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError { + CocoaError(code, url: url, variant: variant, source: source, destination: destination, debugDescription: debugDescription) } } @@ -81,21 +81,21 @@ extension POSIXError { } extension CocoaError { - static func errorWithFilePath(_ pathOrURL: PathOrURL, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError { + static func errorWithFilePath(_ pathOrURL: PathOrURL, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError { switch pathOrURL { case .path(let path): - return Self.errorWithFilePath(path, errno: errno, reading: reading, variant: variant, source: source, destination: destination) + return Self.errorWithFilePath(path, errno: errno, reading: reading, variant: variant, source: source, destination: destination, debugDescription: debugDescription) case .url(let url): - return Self.errorWithFilePath(url, errno: errno, reading: reading, variant: variant, source: source, destination: destination) + return Self.errorWithFilePath(url, errno: errno, reading: reading, variant: variant, source: source, destination: destination, debugDescription: debugDescription) } } - static func errorWithFilePath(_ path: String, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError { - CocoaError(Code(fileErrno: errno, reading: reading), path: path, underlying: POSIXError(errno: errno), variant: variant, source: source, destination: destination) + static func errorWithFilePath(_ path: String, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError { + CocoaError(Code(fileErrno: errno, reading: reading), path: path, underlying: POSIXError(errno: errno), variant: variant, source: source, destination: destination, debugDescription: debugDescription) } - static func errorWithFilePath(_ url: URL, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError { - CocoaError(Code(fileErrno: errno, reading: reading), url: url, underlying: POSIXError(errno: errno), variant: variant, source: source, destination: destination) + static func errorWithFilePath(_ url: URL, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError { + CocoaError(Code(fileErrno: errno, reading: reading), url: url, underlying: POSIXError(errno: errno), variant: variant, source: source, destination: destination, debugDescription: debugDescription) } } @@ -144,18 +144,18 @@ extension CocoaError.Code { } extension CocoaError { - static func errorWithFilePath(_ path: PathOrURL, win32 dwError: DWORD, reading: Bool) -> CocoaError { + static func errorWithFilePath(_ path: PathOrURL, win32 dwError: DWORD, reading: Bool, debugDescription: String? = nil) -> CocoaError { switch path { case let .path(path): - return CocoaError(.init(win32: dwError, reading: reading, emptyPath: path.isEmpty), path: path, underlying: Win32Error(dwError)) + return CocoaError(.init(win32: dwError, reading: reading, emptyPath: path.isEmpty), path: path, underlying: Win32Error(dwError), debugDescription: debugDescription) case let .url(url): let pathStr = url.withUnsafeFileSystemRepresentation { String(cString: $0!) } - return CocoaError(.init(win32: dwError, reading: reading, emptyPath: pathStr.isEmpty), path: pathStr, url: url, underlying: Win32Error(dwError)) + return CocoaError(.init(win32: dwError, reading: reading, emptyPath: pathStr.isEmpty), path: pathStr, url: url, underlying: Win32Error(dwError), debugDescription: debugDescription) } } - static func errorWithFilePath(_ path: String? = nil, win32 dwError: DWORD, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError { - return CocoaError(.init(win32: dwError, reading: reading, emptyPath: path?.isEmpty), path: path, underlying: Win32Error(dwError), variant: variant, source: source, destination: destination) + static func errorWithFilePath(_ path: String? = nil, win32 dwError: DWORD, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError { + return CocoaError(.init(win32: dwError, reading: reading, emptyPath: path?.isEmpty), path: path, underlying: Win32Error(dwError), variant: variant, source: source, destination: destination, debugDescription: debugDescription) } } #endif @@ -190,7 +190,8 @@ extension CocoaError { underlying: (some Error)? = Optional.none, variant: String? = nil, source: String? = nil, - destination: String? = nil + destination: String? = nil, + debugDescription: String? = nil ) { self.init( code, @@ -199,7 +200,8 @@ extension CocoaError { underlying: underlying, variant: variant, source: source, - destination: destination + destination: destination, + debugDescription: debugDescription ) } @@ -209,7 +211,8 @@ extension CocoaError { underlying: (some Error)? = Optional.none, variant: String? = nil, source: String? = nil, - destination: String? = nil + destination: String? = nil, + debugDescription: String? = nil ) { self.init( code, @@ -218,7 +221,8 @@ extension CocoaError { underlying: underlying, variant: variant, source: source, - destination: destination + destination: destination, + debugDescription: debugDescription ) } @@ -229,10 +233,11 @@ extension CocoaError { underlying: (some Error)? = Optional.none, variant: String? = nil, source: String? = nil, - destination: String? = nil + destination: String? = nil, + debugDescription: String? = nil ) { #if FOUNDATION_FRAMEWORK - self.init(_uncheckedNSError: NSError._cocoaError(withCode: code.rawValue, path: path, url: url, underlying: underlying, variant: variant, source: source, destination: destination) as NSError) + self.init(_uncheckedNSError: NSError._cocoaError(withCode: code.rawValue, path: path, url: url, underlying: underlying, variant: variant, source: source, destination: destination, debugDescription: debugDescription) as NSError) #else var userInfo: [String : Any] = [:] if let path { @@ -253,6 +258,9 @@ extension CocoaError { if let variant { userInfo[NSUserStringVariantErrorKey] = [variant] } + if let debugDescription { + userInfo[NSDebugDescriptionErrorKey] = debugDescription + } self.init(code, userInfo: userInfo) #endif diff --git a/Sources/FoundationEssentials/NotificationCenter/AsyncMessage+AsyncSequence.swift b/Sources/FoundationEssentials/NotificationCenter/AsyncMessage+AsyncSequence.swift index aedab64d0..41eac0e7c 100644 --- a/Sources/FoundationEssentials/NotificationCenter/AsyncMessage+AsyncSequence.swift +++ b/Sources/FoundationEssentials/NotificationCenter/AsyncMessage+AsyncSequence.swift @@ -34,7 +34,7 @@ extension NotificationCenter { of subject: Message.Subject, for identifier: Identifier, bufferSize limit: Int = 10 - ) -> some AsyncSequence where Identifier.MessageType == Message, Message.Subject: AnyObject { + ) -> some AsyncSequence & Sendable where Identifier.MessageType == Message, Message.Subject: AnyObject { return AsyncMessageSequence(self, subject, limit) } @@ -48,7 +48,7 @@ extension NotificationCenter { of subject: Message.Subject.Type, for identifier: Identifier, bufferSize limit: Int = 10 - ) -> some AsyncSequence where Identifier.MessageType == Message { + ) -> some AsyncSequence & Sendable where Identifier.MessageType == Message { return AsyncMessageSequence(self, nil, limit) } @@ -62,7 +62,7 @@ extension NotificationCenter { of subject: Message.Subject? = nil, for messageType: Message.Type, bufferSize limit: Int = 10 - ) -> some AsyncSequence where Message.Subject: AnyObject { + ) -> some AsyncSequence & Sendable where Message.Subject: AnyObject { return AsyncMessageSequence(self, subject, limit) } } diff --git a/Sources/FoundationEssentials/Predicate/Archiving/PredicateCodableConfiguration.swift b/Sources/FoundationEssentials/Predicate/Archiving/PredicateCodableConfiguration.swift index f788cea8d..d4fab3740 100644 --- a/Sources/FoundationEssentials/Predicate/Archiving/PredicateCodableConfiguration.swift +++ b/Sources/FoundationEssentials/Predicate/Archiving/PredicateCodableConfiguration.swift @@ -310,7 +310,7 @@ extension PredicateCodableConfiguration { guard root == rootReflectionType.partial, let constructed = constructor(rootReflectionType.genericArguments) else { return nil } - constructed._validateForPredicateUsage(restrictArguments: false) + constructed._validateForPredicateUsage() return constructed } } diff --git a/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift b/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift index b0aef8b2c..f0d204931 100644 --- a/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift +++ b/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift @@ -19,6 +19,8 @@ extension UInt32 { private static var COMPUTED_COMPONENT_PAYLOAD_ARGUMENTS_MASK: UInt32 { 0x0008_0000 } private static var COMPUTED_COMPONENT_PAYLOAD_SETTABLE_MASK: UInt32 { 0x0040_0000 } + private static var STORED_COMPONENT_PAYLOAD_MAXIMUM_INLINE_OFFSET: UInt32 { 0x007F_FFFC } + fileprivate var _keyPathHeader_bufferSize: Int { Int(self & Self.KEYPATH_HEADER_BUFFER_SIZE_MASK) } @@ -31,6 +33,11 @@ extension UInt32 { self & Self.COMPONENT_HEADER_PAYLOAD_MASK } + fileprivate var _keyPathComponentHeader_storedIsInline: Bool { + // If the payload value is greater than the maximum inline offset then it is one of the out-of-line sentinel values + _keyPathComponentHeader_payload <= Self.STORED_COMPONENT_PAYLOAD_MAXIMUM_INLINE_OFFSET + } + fileprivate var _keyPathComponentHeader_computedHasArguments: Bool { (_keyPathComponentHeader_payload & Self.COMPUTED_COMPONENT_PAYLOAD_ARGUMENTS_MASK) != 0 } @@ -40,14 +47,10 @@ extension UInt32 { } } -private func _keyPathOffset(_ root: T.Type, _ keyPath: AnyKeyPath) -> Int? { - MemoryLayout.offset(of: keyPath as! PartialKeyPath) -} - extension AnyKeyPath { private static var WORD_SIZE: Int { MemoryLayout.size } - func _validateForPredicateUsage(restrictArguments: Bool = false) { + func _validateForPredicateUsage() { var ptr = unsafeBitCast(self, to: UnsafeRawPointer.self) ptr = ptr.advanced(by: Self.WORD_SIZE * 3) // skip isa, type metadata, and KVC string pointers let header = ptr.load(as: UInt32.self) @@ -57,11 +60,10 @@ extension AnyKeyPath { case 1: // struct/tuple/self stored property fallthrough case 3: // class stored property - // Key paths to stored properties are only single-component if MemoryLayout.offset(of:) returns an offset - func project(_: T.Type) -> Bool { - _keyPathOffset(T.self, self) == nil - } - if _openExistential(Self.rootType, do: project) { + // Stored property components are either just the payload, or the payload plus 32 bits if the offset is not stored in-line + // Note: we cannot use MemoryLayout.offset(of:) here because not all single-component keypaths have direct offsets (for example, stored properties in final classes) + let size = (firstComponentHeader._keyPathComponentHeader_storedIsInline) ? MemoryLayout.size : MemoryLayout.size + if header._keyPathHeader_bufferSize > size { fatalError("Predicate does not support keypaths with multiple components") } case 2: // computed @@ -70,9 +72,7 @@ extension AnyKeyPath { componentWords += 1 } if firstComponentHeader._keyPathComponentHeader_computedHasArguments { - if restrictArguments { - fatalError("Predicate does not support keypaths with arguments") - } + // TODO: Ensure KeyPath only contains generic arguments and not subscript arguments (https://github.com/swiftlang/swift-foundation/issues/1482) let capturesSize = ptr.advanced(by: Self.WORD_SIZE * componentWords).load(as: UInt.self) componentWords += 2 + (Int(capturesSize) / Self.WORD_SIZE) } diff --git a/Sources/FoundationEssentials/WinSDK+Extensions.swift b/Sources/FoundationEssentials/WinSDK+Extensions.swift index 54f3cb78b..6322c4c86 100644 --- a/Sources/FoundationEssentials/WinSDK+Extensions.swift +++ b/Sources/FoundationEssentials/WinSDK+Extensions.swift @@ -41,6 +41,10 @@ package var CREATE_NEW: DWORD { DWORD(WinSDK.CREATE_NEW) } +package var DELETE: DWORD { + DWORD(WinSDK.DELETE) +} + package var ERROR_ACCESS_DENIED: DWORD { DWORD(WinSDK.ERROR_ACCESS_DENIED) } @@ -133,6 +137,7 @@ package var FILE_ATTRIBUTE_READONLY: DWORD { DWORD(WinSDK.FILE_ATTRIBUTE_READONLY) } + package var FILE_ATTRIBUTE_REPARSE_POINT: DWORD { DWORD(WinSDK.FILE_ATTRIBUTE_REPARSE_POINT) } @@ -153,6 +158,14 @@ package var FILE_NAME_NORMALIZED: DWORD { DWORD(WinSDK.FILE_NAME_NORMALIZED) } +package var FILE_RENAME_FLAG_POSIX_SEMANTICS: DWORD { + DWORD(WinSDK.FILE_RENAME_FLAG_POSIX_SEMANTICS) +} + +package var FILE_RENAME_FLAG_REPLACE_IF_EXISTS: DWORD { + DWORD(WinSDK.FILE_RENAME_FLAG_REPLACE_IF_EXISTS) +} + package var FILE_SHARE_DELETE: DWORD { DWORD(WinSDK.FILE_SHARE_DELETE) } diff --git a/Tests/FoundationEssentialsTests/DataIOTests.swift b/Tests/FoundationEssentialsTests/DataIOTests.swift index e9d2d8e15..4ebca2557 100644 --- a/Tests/FoundationEssentialsTests/DataIOTests.swift +++ b/Tests/FoundationEssentialsTests/DataIOTests.swift @@ -193,6 +193,24 @@ private final class DataIOTests { let maps = try String(contentsOfFile: "/proc/self/maps", encoding: .utf8) #expect(!maps.isEmpty) } + + @Test + func atomicWrite() async throws { + let data = generateTestData() + + await withThrowingTaskGroup(of: Void.self) { group in + for _ in 0 ..< 8 { + group.addTask { [url] in + #expect(throws: Never.self) { + try data.write(to: url, options: [.atomic]) + } + } + } + } + + let readData = try Data(contentsOf: url, options: []) + #expect(readData == data) + } } extension LargeDataTests { diff --git a/Tests/FoundationEssentialsTests/DataTests.swift b/Tests/FoundationEssentialsTests/DataTests.swift index 3b5798b25..69a63eb42 100644 --- a/Tests/FoundationEssentialsTests/DataTests.swift +++ b/Tests/FoundationEssentialsTests/DataTests.swift @@ -46,6 +46,14 @@ extension Data { } } +func createSomeData(_ length: Int) -> Data { + var d = Data(repeating: 42, count: length) + // Set a byte to be another value just so we know we have a unique pointer to the backing + // For maximum inefficiency in the not equal case, set the last byte + d[length - 1] = UInt8.random(in: UInt8.min.. Predicate +} + +fileprivate struct PredicateProducerConformer : PredicateProducer { + let prop = 2 +} + +extension PredicateProducer { + func getPredicate() -> Predicate { + #Predicate { $0.prop == 2 } + } +} + @Suite("Predicate") private struct PredicateTests { typealias Object = PredicateTestObject @@ -310,6 +326,123 @@ private struct PredicateTests { $0.a == $0.c && $0.b == now } } + + @Test func finalKeyPaths() { + final class Foo { + var id: Int = 1 + } + _ = #Predicate { $0.id == 2 } + } + + @Test func genericKeyPaths() { + let obj = PredicateProducerConformer() + // Ensure forming a predicate to a generic type does not cause crashes when validating keypaths + _ = obj.getPredicate() + } + +#if FOUNDATION_EXIT_TESTS + @Test func unsupportedKeyPaths() async { + struct Sample { + let stored: Sample2 + var immutableComputed: Sample2 { fatalError() } + var mutableComputed: Sample2 { + get { fatalError() } + set { fatalError() } + } + var optional: Sample2? { fatalError() } + + subscript(_ arg: Int) -> Sample2 { fatalError() } + subscript() -> Sample2 { fatalError() } + } + + struct Sample2 { + var prop: Int + } + + // multiple components + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample.stored.prop + ) + } + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample.immutableComputed.prop + ) + } + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample.mutableComputed.prop + ) + } + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample.optional?.prop + ) + } + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample.[1].prop + ) + } + + // subscripts with arguments + // This keypath is currently allow but should be considered invalid (https://github.com/swiftlang/swift-foundation/issues/1482) + #if false + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample.[0] + ) + } + #endif + + // Optional chaining + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample.optional? + ) + } + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample?.? + ) + } + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample?.?.stored + ) + } + + // Force unwrapping + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample.optional! + ) + } + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample?.! + ) + } + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample?.!.stored + ) + } + } +#endif @Test func regex() throws { diff --git a/Tests/FoundationMacrosTests/MacroTestUtilities.swift b/Tests/FoundationMacrosTests/MacroTestUtilities.swift index bfc544e82..cb11e28a2 100644 --- a/Tests/FoundationMacrosTests/MacroTestUtilities.swift +++ b/Tests/FoundationMacrosTests/MacroTestUtilities.swift @@ -47,9 +47,9 @@ struct DiagnosticTest : ExpressibleByStringLiteral, Hashable, CustomStringConver var mappedToExpression: Self { DiagnosticTest( - message._replacing("Predicate", with: "Expression")._replacing("predicate", with: "expression"), + message.replacing("Predicate", with: "Expression").replacing("predicate", with: "expression"), fixIts: fixIts.map { - FixItTest($0.message, result: $0.result._replacing("#Predicate", with: "#Expression")) + FixItTest($0.message, result: $0.result.replacing("#Predicate", with: "#Expression")) } ) } @@ -100,7 +100,7 @@ extension Diagnostic { } else { var result = "Message: \(debugDescription)\nFix-Its:\n" for fixIt in fixIts { - result += "\t\(fixIt.message.message)\n\t\(fixIt.changes.first!._result._replacing("\n", with: "\n\t"))" + result += "\t\(fixIt.message.message)\n\t\(fixIt.changes.first!._result.replacing("\n", with: "\n\t"))" } return result } @@ -114,7 +114,7 @@ extension DiagnosticTest { } else { var result = "Message: \(message)\nFix-Its:\n" for fixIt in fixIts { - result += "\t\(fixIt.message)\n\t\(fixIt.result._replacing("\n", with: "\n\t"))" + result += "\t\(fixIt.message)\n\t\(fixIt.result.replacing("\n", with: "\n\t"))" } return result } @@ -164,21 +164,9 @@ func AssertPredicateExpansion(_ source: String, _ result: String = "", diagnosti ) AssertMacroExpansion( macros: ["Expression" : FoundationMacros.ExpressionMacro.self], - source._replacing("#Predicate", with: "#Expression"), - result._replacing(".Predicate", with: ".Expression"), + source.replacing("#Predicate", with: "#Expression"), + result.replacing(".Predicate", with: ".Expression"), diagnostics: Set(diagnostics.map(\.mappedToExpression)), sourceLocation: sourceLocation ) } - -extension String { - func _replacing(_ text: String, with other: String) -> Self { - if #available(macOS 13.0, *) { - // Use the stdlib API if available - self.replacing(text, with: other) - } else { - // Use the Foundation API on older OSes - self.replacingOccurrences(of: text, with: other, options: [.literal]) - } - } -} diff --git a/Tests/FoundationMacrosTests/PredicateMacroUsageTests.swift b/Tests/FoundationMacrosTests/PredicateMacroUsageTests.swift index 815ca0c80..72bb4b6c5 100644 --- a/Tests/FoundationMacrosTests/PredicateMacroUsageTests.swift +++ b/Tests/FoundationMacrosTests/PredicateMacroUsageTests.swift @@ -21,14 +21,12 @@ import Foundation fileprivate func _blackHole(_ t: T) {} @inline(never) -@available(macOS 14, iOS 17, watchOS 10, tvOS 17, *) fileprivate func _blackHoleExplicitInput(_ predicate: Predicate) {} // MARK: - Tests @Suite("#Predicate Macro Usage") private struct PredicateMacroUsageTests { - @available(macOS 14, iOS 17, watchOS 10, tvOS 17, *) @Test func usage() { _blackHole(#Predicate { return $0