From 09b7a58dd8f95377f79985088e6c2949fad77e90 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Tue, 12 Aug 2025 13:42:03 -0700 Subject: [PATCH 01/14] (158139242) Prevent encodingDateFormatted() from using the current internationalization preferences (#1467) --- Tests/FoundationEssentialsTests/JSONEncoderTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift index 466dcceff..696ecb117 100644 --- a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift +++ b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift @@ -2896,6 +2896,11 @@ extension JSONEncoderTests { let formatter = DateFormatter() formatter.dateStyle = .full formatter.timeStyle = .full + var cal = Calendar(identifier: .gregorian) + cal.timeZone = .gmt + formatter.calendar = cal + formatter.locale = Locale(identifier: "en_US") + formatter.timeZone = .gmt let timestamp = Date(timeIntervalSince1970: 1000) let expectedJSON = "\"\(formatter.string(from: timestamp))\"".data(using: .utf8)! From a8bee5bfc71210168fa1b973fb1a1deb8bde2047 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Wed, 13 Aug 2025 10:07:27 -0700 Subject: [PATCH 02/14] (157858997) Cleanup FoundationMacros availability checks (#1469) --- .../MacroTestUtilities.swift | 24 +++++-------------- .../PredicateMacroUsageTests.swift | 2 -- 2 files changed, 6 insertions(+), 20 deletions(-) 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 From 393dabc3298c2077e311e8b0de4add0bbffa97a8 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Mon, 18 Aug 2025 10:01:31 -0700 Subject: [PATCH 03/14] Update default attribute scope framework paths (#1472) Co-authored-by: Rafael Cerioli --- .../AttributedString/AttributeScope.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 ( From 2818d5bc3580af4912916ae53699ed34b5bb2d32 Mon Sep 17 00:00:00 2001 From: Christopher Thielen <77445+cthielen@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:23:49 -0700 Subject: [PATCH 04/14] NotificationCenter.messages() should return Sendable type (#1473) --- .../NotificationCenter/AsyncMessage+AsyncSequence.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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) } } From 473b0d1a442750776bfb9b4d39778a286f7dd4ee Mon Sep 17 00:00:00 2001 From: noppe Date: Wed, 20 Aug 2025 02:38:11 +0900 Subject: [PATCH 05/14] Update 0023-progress-reporter.md (#1466) --- Proposals/0023-progress-reporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 92b1b021856978765a9c115bc1d09b7c5d1ce34a Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Wed, 20 Aug 2025 12:20:08 -0700 Subject: [PATCH 06/14] Fix crash when using keypaths on final classes in Predicates (#1475) --- .../Predicate/KeyPath+Inspection.swift | 20 ++++++++++--------- .../PredicateTests.swift | 7 +++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift b/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift index b0aef8b2c..51f945765 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,10 +47,6 @@ 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 } @@ -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 diff --git a/Tests/FoundationEssentialsTests/PredicateTests.swift b/Tests/FoundationEssentialsTests/PredicateTests.swift index 2a4a5541a..98c68be3f 100644 --- a/Tests/FoundationEssentialsTests/PredicateTests.swift +++ b/Tests/FoundationEssentialsTests/PredicateTests.swift @@ -310,6 +310,13 @@ 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 regex() throws { From 4cdbaf60e28cd554ff09a679bed32cb6f4bce130 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 21 Aug 2025 13:24:16 -0700 Subject: [PATCH 07/14] Add predicate keypath assertion exit tests (#1476) --- .../Predicate/KeyPath+Inspection.swift | 2 +- .../PredicateTests.swift | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift b/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift index 51f945765..3fa824e44 100644 --- a/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift +++ b/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift @@ -50,7 +50,7 @@ extension UInt32 { extension AnyKeyPath { private static var WORD_SIZE: Int { MemoryLayout.size } - func _validateForPredicateUsage(restrictArguments: Bool = false) { + func _validateForPredicateUsage(restrictArguments: Bool = true) { 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) diff --git a/Tests/FoundationEssentialsTests/PredicateTests.swift b/Tests/FoundationEssentialsTests/PredicateTests.swift index 98c68be3f..3eea7d077 100644 --- a/Tests/FoundationEssentialsTests/PredicateTests.swift +++ b/Tests/FoundationEssentialsTests/PredicateTests.swift @@ -317,6 +317,107 @@ private struct PredicateTests { } _ = #Predicate { $0.id == 2 } } + +#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 + await #expect(processExitsWith: .failure) { + _ = PredicateExpressions.KeyPath( + root: PredicateExpressions.Variable(), + keyPath: \Sample.[0] + ) + } + + // 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 { From 1734c7b63b681f5457a9ff252526786c8bc38df5 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 21 Aug 2025 13:52:44 -0700 Subject: [PATCH 08/14] Improve temporary file location for atomic writes (#1477) * (156721664) Temporary files for atomic writes should use _amkrtemp instead of mktemp when applicable * Fix build failure --- .../Data/Data+Writing.swift | 49 +++++++++++++---- .../Error/CocoaError+FilePath.swift | 52 +++++++++++-------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 51b54ed5e..a96d0f7c3 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) } 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 From 574d608a30b30ec829984e9ece6834966213b1ab Mon Sep 17 00:00:00 2001 From: Charles Hu Date: Thu, 21 Aug 2025 14:57:59 -0700 Subject: [PATCH 09/14] Expose Decimal.pow() as a public API on FoundationEssentials (#1471) --- .../Decimal/Decimal+Compatibility.swift | 10 ---------- 1 file changed, 10 deletions(-) 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, From b60b6e9c9656444fbe6c17d89a0a6a94967d9109 Mon Sep 17 00:00:00 2001 From: Tina L <49205802+itingliu@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:09:44 -0700 Subject: [PATCH 10/14] Update status and proposal name (#1478) --- Proposals/{NNNN-random-uuid.md => 0031-random-uuid.md} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename Proposals/{NNNN-random-uuid.md => 0031-random-uuid.md} (91%) 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 From 4e013668a999a01b9cca29473a2c687e707f23cd Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Sat, 23 Aug 2025 01:10:38 +0200 Subject: [PATCH 11/14] [Proposal] Base64 urlencoding and omitting padding options (#1156) * Pitch: Base64 urlencoding and omitting padding options * Fix `base64URLAlphabet` * Update proposal * Update Proposal * Update Proposal number --- ...base64-urlencoding-and-omitting-padding.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 Proposals/0030-base64-urlencoding-and-omitting-padding.md 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 From 081798ca6b6a74b0f3c8b609a257c52ec0c3b714 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Wed, 27 Aug 2025 12:40:52 -0700 Subject: [PATCH 12/14] Fix crash when creating predicate with generic root type (#1483) --- .../PredicateCodableConfiguration.swift | 2 +- .../Predicate/KeyPath+Inspection.swift | 6 ++--- .../PredicateTests.swift | 25 +++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) 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 3fa824e44..f0d204931 100644 --- a/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift +++ b/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift @@ -50,7 +50,7 @@ extension UInt32 { extension AnyKeyPath { private static var WORD_SIZE: Int { MemoryLayout.size } - func _validateForPredicateUsage(restrictArguments: Bool = true) { + 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) @@ -72,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/Tests/FoundationEssentialsTests/PredicateTests.swift b/Tests/FoundationEssentialsTests/PredicateTests.swift index 3eea7d077..b16223c3e 100644 --- a/Tests/FoundationEssentialsTests/PredicateTests.swift +++ b/Tests/FoundationEssentialsTests/PredicateTests.swift @@ -39,6 +39,22 @@ struct PredicateTestObject2 { var a: Bool } + +fileprivate protocol PredicateProducer { + var prop: Int { get } + func getPredicate() -> 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 @@ -318,6 +334,12 @@ private struct PredicateTests { _ = #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 { @@ -370,12 +392,15 @@ private struct PredicateTests { } // 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) { From ca4ee3ca1337a264a8dde07af439ba4d728a2857 Mon Sep 17 00:00:00 2001 From: Saleem Abdulrasool Date: Tue, 2 Sep 2025 10:03:00 -0700 Subject: [PATCH 13/14] FoundationEssentials: try to make the file atomic behaviour more robust (#1485) We have observed `ERROR_ACCESS_DENIED` in CI on `SetRenameInformationFile`. Try to make the path more robust by first performing a kernel based rename with POSIX semantics. This requires Windows 10 1809+. If that is unsuccessful, verify that attributes are not to blame. A failure may still occur if the file is on a different volume. In such a case, fallback to the `MoveFileExW` operation to perform a copy + delete operation. Hopefully this should make the implementation more robust to failures. --- .../Data/Data+Writing.swift | 94 ++++++++++++++----- .../WinSDK+Extensions.swift | 13 +++ .../DataIOTests.swift | 18 ++++ 3 files changed, 100 insertions(+), 25 deletions(-) diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 51b54ed5e..720923064 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -358,44 +358,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) + } - _close(fd) - fd = -1 + if !SetFileInformationByHandle(hFile, FileRenameInfoEx, pInfo, dwSize) { + let dwError = GetLastError() + guard dwError == ERROR_NOT_SAME_DEVICE else { + throw CocoaError.errorWithFilePath(inPath, win32: dwError, reading: false) + } - 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) + _ = CloseHandle(hFile) + hFile = INVALID_HANDLE_VALUE + + // 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/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 { From 5c12225d8229ade9776f87d8f86926480d7afb32 Mon Sep 17 00:00:00 2001 From: Rick van Voorden Date: Thu, 26 Jun 2025 16:17:04 -0700 Subject: [PATCH 14/14] data identical --- .../Essentials/BenchmarkEssentials.swift | 38 +++++++++++++- Sources/FoundationEssentials/Data/Data.swift | 50 +++++++++++++++++++ .../FoundationEssentialsTests/DataTests.swift | 18 +++++++ 3 files changed, 105 insertions(+), 1 deletion(-) 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/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/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..