diff --git a/CMakeLists.txt b/CMakeLists.txt index 31638c287..ccbfd3c69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,7 @@ list(APPEND _SwiftFoundation_versions "6.1" "6.2" "6.3" + "6.4" ) # Each availability name to define diff --git a/Package.swift b/Package.swift index d0c25c67d..448343e7b 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ import CompilerPluginSupport let availabilityTags: [_Availability] = [ _Availability("FoundationPreview"), // Default FoundationPreview availability ] -let versionNumbers = ["6.0.2", "6.1", "6.2", "6.3"] +let versionNumbers = ["6.0.2", "6.1", "6.2", "6.3", "6.4"] // Availability Macro Utilities @@ -140,7 +140,8 @@ let package = Package( "ProcessInfo/CMakeLists.txt", "FileManager/CMakeLists.txt", "URL/CMakeLists.txt", - "NotificationCenter/CMakeLists.txt" + "NotificationCenter/CMakeLists.txt", + "ProgressManager/CMakeLists.txt", ], cSettings: [ .define("_GNU_SOURCE", .when(platforms: [.linux])) @@ -185,7 +186,7 @@ let package = Package( "Locale/CMakeLists.txt", "Calendar/CMakeLists.txt", "CMakeLists.txt", - "Predicate/CMakeLists.txt" + "Predicate/CMakeLists.txt", ], cSettings: wasiLibcCSettings, swiftSettings: [ diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index d009cde11..2f04ff0be 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(FoundationEssentials Logging.swift OutputBuffer.swift Platform.swift + Progress+Stub.swift SortComparator.swift UUID_Wrappers.swift UUID.swift @@ -45,6 +46,7 @@ add_subdirectory(Locale) add_subdirectory(NotificationCenter) add_subdirectory(Predicate) add_subdirectory(ProcessInfo) +add_subdirectory(ProgressManager) add_subdirectory(PropertyList) add_subdirectory(String) add_subdirectory(TimeZone) diff --git a/Sources/FoundationEssentials/Data/CMakeLists.txt b/Sources/FoundationEssentials/Data/CMakeLists.txt index 6667ad4d9..cc681f603 100644 --- a/Sources/FoundationEssentials/Data/CMakeLists.txt +++ b/Sources/FoundationEssentials/Data/CMakeLists.txt @@ -2,7 +2,7 @@ ## ## This source file is part of the Swift open source project ## -## Copyright (c) 2024 Apple Inc. and the Swift project authors +## Copyright (c) 2024-2025 Apple Inc. and the Swift project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -13,13 +13,23 @@ ##===----------------------------------------------------------------------===## target_sources(FoundationEssentials PRIVATE + Representations/Data+Inline.swift + Representations/Data+InlineSlice.swift + Representations/Data+LargeSlice.swift + Representations/Data+Representation.swift + Representations/DataStorage.swift + Collections+DataProtocol.swift ContiguousBytes.swift + Data.swift Data+Base64.swift + Data+Bridging.swift + Data+Deprecated.swift Data+Error.swift + Data+Iterator.swift Data+Reading.swift - Data+Stub.swift + Data+Searching.swift Data+Writing.swift - Data.swift DataProtocol.swift + PathOrURL.swift Pointers+DataProtocol.swift) diff --git a/Sources/FoundationEssentials/Data/ContiguousBytes.swift b/Sources/FoundationEssentials/Data/ContiguousBytes.swift index c7569c6d7..c81c8d00f 100644 --- a/Sources/FoundationEssentials/Data/ContiguousBytes.swift +++ b/Sources/FoundationEssentials/Data/ContiguousBytes.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2018 Apple Inc. and the Swift project authors +// Copyright (c) 2018-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -39,6 +39,9 @@ extension ArraySlice : ContiguousBytes where Element == UInt8 { } @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension ContiguousArray : ContiguousBytes where Element == UInt8 { } +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data : ContiguousBytes { } + //===--- Pointer Conformances ---------------------------------------------===// @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) diff --git a/Sources/FoundationEssentials/Data/Data+Base64.swift b/Sources/FoundationEssentials/Data/Data+Base64.swift index bb1d64f60..bdc8985f1 100644 --- a/Sources/FoundationEssentials/Data/Data+Base64.swift +++ b/Sources/FoundationEssentials/Data/Data+Base64.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2023-2024 Apple Inc. and the Swift project authors +// Copyright (c) 2023-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -25,6 +25,56 @@ import WinSDK import WASILibc #endif +#if !FOUNDATION_FRAMEWORK +extension Data { + + @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) + public struct Base64EncodingOptions : OptionSet, Sendable { + public let rawValue: UInt + + public init(rawValue: UInt) { + self.rawValue = rawValue + } + /// Set the maximum line length to 64 characters, after which a line ending is inserted. + public static let lineLength64Characters = Base64EncodingOptions(rawValue: 1 << 0) + /// Set the maximum line length to 76 characters, after which a line ending is inserted. + public static let lineLength76Characters = Base64EncodingOptions(rawValue: 1 << 1) + /// When a maximum line length is set, specify that the line ending to insert should include a carriage return. + public static let endLineWithCarriageReturn = Base64EncodingOptions(rawValue: 1 << 4) + /// When a maximum line length is set, specify that the line ending to insert should include a line feed. + public static let endLineWithLineFeed = Base64EncodingOptions(rawValue: 1 << 5) + } + + @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) + public struct Base64DecodingOptions : OptionSet, Sendable { + public let rawValue: UInt + + public init(rawValue: UInt) { + self.rawValue = rawValue + } + /// Modify the decoding algorithm so that it ignores unknown non-Base-64 bytes, including line ending characters. + public static let ignoreUnknownCharacters = Base64DecodingOptions(rawValue: 1 << 0) + } +} +#else +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { + // These types are typealiased to the `NSData` options for framework builds only. + public typealias Base64EncodingOptions = NSData.Base64EncodingOptions + public typealias Base64DecodingOptions = NSData.Base64DecodingOptions +} +#endif //!FOUNDATION_FRAMEWORK + +extension Data.Base64EncodingOptions { + /// Use the base64url alphabet to encode the data + @available(FoundationPreview 6.3, *) + public static let base64URLAlphabet = Self(rawValue: 1 << 6) + + /// Omit the `=` padding characters in the end of the base64 encoded result + @available(FoundationPreview 6.3, *) + public static let omitPaddingCharacter = Self(rawValue: 1 << 7) +} + @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension Data { diff --git a/Sources/FoundationEssentials/Data/Data+Bridging.swift b/Sources/FoundationEssentials/Data/Data+Bridging.swift new file mode 100644 index 000000000..e437a14a8 --- /dev/null +++ b/Sources/FoundationEssentials/Data/Data+Bridging.swift @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2022-2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK + +internal import _ForSwiftFoundation + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension __DataStorage { + @inline(never) // This is not @inlinable to avoid emission of the private `__NSSwiftData` class name into clients. + @usableFromInline + func bridgedReference(_ range: Range) -> AnyObject { + if range.isEmpty { + return NSData() // zero length data can be optimized as a singleton + } + + return __NSSwiftData(backing: self, range: range) + } +} + +// NOTE: older overlays called this _NSSwiftData. The two must +// coexist, so it was renamed. The old name must not be used in the new +// runtime. +internal final class __NSSwiftData : NSData { + var _backing: __DataStorage! + var _range: Range! + + convenience init(backing: __DataStorage, range: Range) { + self.init() + _backing = backing + _range = range + } + @objc override var length: Int { + return _range.upperBound - _range.lowerBound + } + + @objc override var bytes: UnsafeRawPointer { + // NSData's byte pointer methods are not annotated for nullability correctly + // (but assume non-null by the wrapping macro guards). This placeholder value + // is to work-around this bug. Any indirection to the underlying bytes of an NSData + // with a length of zero would have been a programmer error anyhow so the actual + // return value here is not needed to be an allocated value. This is specifically + // needed to live like this to be source compatible with Swift3. Beyond that point + // this API may be subject to correction. + guard let bytes = _backing.bytes else { + return UnsafeRawPointer(bitPattern: 0xBAD0)! + } + + return bytes.advanced(by: _range.lowerBound) + } + + @objc override func copy(with zone: NSZone? = nil) -> Any { + if _backing._copyWillRetain { + return self + } else { + return NSData(bytes: bytes, length: length) + } + + } + + @objc override func mutableCopy(with zone: NSZone? = nil) -> Any { + return NSMutableData(bytes: bytes, length: length) + } + + @objc override + func _isCompact() -> Bool { + return true + } + + @objc override + func _bridgingCopy(_ bytes: UnsafeMutablePointer, length: UnsafeMutablePointer) -> Data? { + fatalError("Unexpected call to __NSSwiftData._bridgingCopy(_:length:)") + } +} + +extension Data { + internal func _bridgeToObjectiveCImpl() -> AnyObject { + switch _representation { + case .empty: return NSData() + case .inline(let inline): + return inline.withUnsafeBytes { + return NSData(bytes: $0.baseAddress, length: $0.count) + } + case .slice(let slice): + return slice.storage.bridgedReference(slice.range) + case .large(let slice): + return slice.storage.bridgedReference(slice.range) + } + } + + internal static func _bridgeFromObjectiveCAdoptingNativeStorageOf(_ source: AnyObject) -> Data? { + guard object_getClass(source) == __NSSwiftData.self else { return nil } + + let swiftData = unsafeDowncast(source, to: __NSSwiftData.self) + let range = swiftData._range! + let originalBacking = swiftData._backing! + + // (rdar://162776451) Some clients assume that the double-bridged Data's start index is 0 due to historical behavior. We need to make sure the created Data's indices begin at 0 rather than preserving the original offset/slice range here. This requires creating a new __DataStorage instead of using the existing one. + // (rdar://121865256) We also need to make sure that we don't create a new __DataStorage that holds on to the original via the deallocator. If a value is double bridged repeatedly (as is the case in some clients), unwinding in the dealloc can cause a stack overflow. This requires either using the existing __DataStorage, or creating a new one with a copy of the bytes to avoid a deallocator chain. + // Based on the two constraints above, we perform a copy here. Ideally in the future if we remove the first constraint we could re-use the existing originalBacking to avoid the copy. + let newBacking = __DataStorage(bytes: originalBacking.mutableBytes?.advanced(by: range.lowerBound), length: range.count) + + if InlineSlice.canStore(count: newBacking.length) { + return Data(representation: .slice(InlineSlice(newBacking, count: newBacking.length))) + } else { + return Data(representation: .large(LargeSlice(newBacking, count: newBacking.length))) + } + } +} + +#endif diff --git a/Sources/FoundationEssentials/Data/Data+Deprecated.swift b/Sources/FoundationEssentials/Data/Data+Deprecated.swift new file mode 100644 index 000000000..d48caff15 --- /dev/null +++ b/Sources/FoundationEssentials/Data/Data+Deprecated.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { + @available(swift, introduced: 4.2) + @available(swift, deprecated: 5, message: "use `init(_:)` instead") + public init(bytes elements: S) where S.Iterator.Element == UInt8 { + self.init(elements) + } + + @available(swift, obsoleted: 4.2) + public init(bytes: Array) { + self.init(bytes) + } + + @available(swift, obsoleted: 4.2) + public init(bytes: ArraySlice) { + self.init(bytes) + } + + /// Access the bytes in the data. + /// + /// - warning: The byte pointer argument should not be stored and used outside of the lifetime of the call to the closure. + @available(swift, deprecated: 5, message: "use `withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` instead") + public func withUnsafeBytes(_ body: (UnsafePointer) throws -> ResultType) rethrows -> ResultType { + return try self.withUnsafeBytes { + return try body($0.baseAddress?.assumingMemoryBound(to: ContentType.self) ?? UnsafePointer(bitPattern: 0xBAD0)!) + } + } + + /// Mutate the bytes in the data. + /// + /// This function assumes that you are mutating the contents. + /// - warning: The byte pointer argument should not be stored and used outside of the lifetime of the call to the closure. + @available(swift, deprecated: 5, message: "use `withUnsafeMutableBytes(_: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R` instead") + public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutablePointer) throws -> ResultType) rethrows -> ResultType { + return try self.withUnsafeMutableBytes { + return try body($0.baseAddress?.assumingMemoryBound(to: ContentType.self) ?? UnsafeMutablePointer(bitPattern: 0xBAD0)!) + } + } + + /// Enumerate the contents of the data. + /// + /// In some cases, (for example, a `Data` backed by a `dispatch_data_t`, the bytes may be stored discontinuously. In those cases, this function invokes the closure for each contiguous region of bytes. + /// - parameter block: The closure to invoke for each region of data. You may stop the enumeration by setting the `stop` parameter to `true`. + @available(swift, deprecated: 5, message: "use `regions` or `for-in` instead") + public func enumerateBytes(_ block: (_ buffer: UnsafeBufferPointer, _ byteIndex: Index, _ stop: inout Bool) -> Void) { + self.withUnsafeBytes { + var stop = false + block($0.assumingMemoryBound(to: UInt8.self), 0, &stop) + } + } +} diff --git a/Sources/FoundationEssentials/Data/Data+Iterator.swift b/Sources/FoundationEssentials/Data/Data+Iterator.swift new file mode 100644 index 000000000..a19662ae8 --- /dev/null +++ b/Sources/FoundationEssentials/Data/Data+Iterator.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { + /// An iterator over the contents of the data. + /// + /// The iterator will increment byte-by-byte. + @inlinable // This is @inlinable as trivially computable. + public func makeIterator() -> Data.Iterator { + return Iterator(self, at: startIndex) + } + + public struct Iterator : IteratorProtocol, Sendable { + @usableFromInline + internal typealias Buffer = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + @usableFromInline internal let _data: Data + @usableFromInline internal var _buffer: Buffer + @usableFromInline internal var _idx: Data.Index + @usableFromInline internal let _endIdx: Data.Index + + @usableFromInline // This is @usableFromInline as a non-trivial initializer. + internal init(_ data: Data, at loc: Data.Index) { + // The let vars prevent this from being marked as @inlinable + _data = data + _buffer = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) + _idx = loc + _endIdx = data.endIndex + + let bufferSize = MemoryLayout.size + Swift.withUnsafeMutableBytes(of: &_buffer) { + $0.withMemoryRebound(to: UInt8.self) { [endIndex = data.endIndex] buf in + let bufferIdx = (loc - data.startIndex) % bufferSize + let end = (endIndex - (loc - bufferIdx) > bufferSize) ? (loc - bufferIdx + bufferSize) : endIndex + data.copyBytes(to: buf, from: (loc - bufferIdx).. UInt8? { + let idx = _idx + let bufferSize = MemoryLayout.size + + guard idx < _endIdx else { return nil } + _idx += 1 + + let bufferIdx = (idx - _data.startIndex) % bufferSize + + + if bufferIdx == 0 { + var buffer = _buffer + Swift.withUnsafeMutableBytes(of: &buffer) { + $0.withMemoryRebound(to: UInt8.self) { + // populate the buffer + _data.copyBytes(to: $0, from: idx..<(_endIdx - idx > bufferSize ? idx + bufferSize : _endIdx)) + } + } + _buffer = buffer + } + + return Swift.withUnsafeMutableBytes(of: &_buffer) { + $0.load(fromByteOffset: bufferIdx, as: UInt8.self) + } + } + } +} diff --git a/Sources/FoundationEssentials/Data/Data+Reading.swift b/Sources/FoundationEssentials/Data/Data+Reading.swift index 7b0d444dd..4dd3714a3 100644 --- a/Sources/FoundationEssentials/Data/Data+Reading.swift +++ b/Sources/FoundationEssentials/Data/Data+Reading.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -193,33 +193,33 @@ struct ReadBytesResult { } #if os(Windows) +@_lifetime(pBuffer: copy pBuffer) private func read(from hFile: HANDLE, at path: PathOrURL, - into pBuffer: UnsafeMutableRawPointer, length dwLength: Int, + into pBuffer: inout OutputRawSpan, chunkSize dwChunk: Int = 4096, progress bProgress: Bool) - throws -> Int { - var pBuffer = pBuffer - let progress = bProgress && Progress.current() != nil ? Progress(totalUnitCount: Int64(dwLength)) : nil + throws { + let progress = bProgress && Progress.current() != nil ? Progress(totalUnitCount: Int64(pBuffer.freeCapacity)) : nil - var dwBytesRemaining: DWORD = DWORD(dwLength) - while dwBytesRemaining > 0 { + while !pBuffer.isFull { if let progress, progress.isCancelled { throw CocoaError(.userCancelled) } let dwBytesToRead: DWORD = - DWORD(clamping: DWORD(min(DWORD(dwChunk), dwBytesRemaining))) + DWORD(clamping: min(dwChunk, pBuffer.freeCapacity)) + var dwBytesRead: DWORD = 0 - if !ReadFile(hFile, pBuffer, dwBytesToRead, &dwBytesRead, nil) { - throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true) + try pBuffer.withUnsafeMutableBytes { bytes, initializedCount in + if !ReadFile(hFile, bytes.baseAddress!.advanced(by: initializedCount), dwBytesToRead, &dwBytesRead, nil) { + throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true) + } + initializedCount += Int(dwBytesRead) } - dwBytesRemaining -= DWORD(clamping: dwBytesRead) - progress?.completedUnitCount = Int64(dwLength - Int(dwBytesRemaining)) + progress?.completedUnitCount += Int64(dwBytesRead) if dwBytesRead < dwBytesToRead { break } - pBuffer = pBuffer.advanced(by: Int(dwBytesRead)) } - return dwLength - Int(dwBytesRemaining) } #endif @@ -283,18 +283,20 @@ internal func readBytesFromFile(path inPath: PathOrURL, reportProgress: Bool, ma } })) } else { - guard let pBuffer: UnsafeMutableRawPointer = malloc(Int(szFileSize)) else { + guard let ptr: UnsafeMutableRawPointer = malloc(Int(szFileSize)) else { throw CocoaError.errorWithFilePath(inPath, errno: ENOMEM, reading: true) } + let buffer = UnsafeMutableRawBufferPointer(start: ptr, count: Int(szFileSize)) + var outputSpan = OutputRawSpan(buffer: buffer, initializedCount: 0) - localProgress?.becomeCurrent(withPendingUnitCount: Int64(szFileSize)) + localProgress?.becomeCurrent(withPendingUnitCount: Int64(outputSpan.freeCapacity)) do { - let dwLength = try read(from: hFile, at: inPath, into: pBuffer, length: Int(szFileSize), progress: reportProgress) + try read(from: hFile, at: inPath, into: &outputSpan, progress: reportProgress) localProgress?.resignCurrent() - return ReadBytesResult(bytes: pBuffer, length: dwLength, deallocator: .free) + return ReadBytesResult(bytes: ptr, length: outputSpan.finalize(for: buffer), deallocator: .free) } catch { localProgress?.resignCurrent() - free(pBuffer) + free(ptr) throw error } } @@ -367,18 +369,21 @@ internal func readBytesFromFile(path inPath: PathOrURL, reportProgress: Bool, ma #if os(Linux) || os(Android) // Linux has some files that may report a size of 0 but actually have contents let chunkSize = 1024 * 4 - var buffer = malloc(chunkSize)! + var ptr = malloc(chunkSize)! var totalRead = 0 while true { - let length = try readBytesFromFileDescriptor(fd, path: inPath, buffer: buffer.advanced(by: totalRead), length: chunkSize, readUntilLength: false, reportProgress: false) + let buffer = UnsafeMutableRawBufferPointer(start: ptr, count: totalRead + chunkSize) + var outputSpan = OutputRawSpan(buffer: buffer, initializedCount: totalRead) + try readBytesFromFileDescriptor(fd, path: inPath, buffer: &outputSpan, readUntilLength: false, reportProgress: false) + let length = outputSpan.finalize(for: buffer) totalRead += length if length != chunkSize { break } - buffer = realloc(buffer, totalRead + chunkSize) + ptr = realloc(ptr, totalRead + chunkSize) } - result = ReadBytesResult(bytes: buffer, length: totalRead, deallocator: .free) + result = ReadBytesResult(bytes: ptr, length: totalRead, deallocator: .free) #else result = ReadBytesResult(bytes: nil, length: 0, deallocator: nil) #endif @@ -415,13 +420,15 @@ internal func readBytesFromFile(path inPath: PathOrURL, reportProgress: Bool, ma guard let bytes = malloc(Int(fileSize)) else { throw CocoaError.errorWithFilePath(inPath, errno: ENOMEM, reading: true) } + let buffer = UnsafeMutableRawBufferPointer(start: bytes, count: Int(fileSize)) + var outputSpan = OutputRawSpan(buffer: buffer, initializedCount: 0) localProgress?.becomeCurrent(withPendingUnitCount: Int64(fileSize)) do { - let length = try readBytesFromFileDescriptor(fd, path: inPath, buffer: bytes, length: fileSize, reportProgress: reportProgress) + try readBytesFromFileDescriptor(fd, path: inPath, buffer: &outputSpan, reportProgress: reportProgress) localProgress?.resignCurrent() - result = ReadBytesResult(bytes: bytes, length: length, deallocator: .free) + result = ReadBytesResult(bytes: bytes, length: outputSpan.finalize(for: buffer), deallocator: .free) } catch { localProgress?.resignCurrent() free(bytes) @@ -440,25 +447,24 @@ internal func readBytesFromFile(path inPath: PathOrURL, reportProgress: Bool, ma /// Read data from a file descriptor. /// Takes an `Int` size and returns an `Int` to match `Data`'s count. If we are going to read more than Int.max, throws - because we won't be able to store it in `Data`. /// If `readUntilLength` is `false`, then we will end the read if we receive less than `length` bytes. This can be used to read from something like a socket, where the `length` simply represents the maximum size you can read at once. -private func readBytesFromFileDescriptor(_ fd: Int32, path: PathOrURL, buffer inBuffer: UnsafeMutableRawPointer, length: Int, readUntilLength: Bool = true, reportProgress: Bool) throws -> Int { - var buffer = inBuffer +private func readBytesFromFileDescriptor(_ fd: Int32, path: PathOrURL, buffer inBuffer: inout OutputRawSpan, readUntilLength: Bool = true, reportProgress: Bool) throws { // If chunkSize (8-byte value) is more than blksize_t.max (4 byte value), then use the 4 byte max and chunk let preferredChunkSize: size_t let localProgress: Progress? + let length = inBuffer.freeCapacity if Progress.current() != nil && reportProgress { - localProgress = Progress(totalUnitCount: Int64(length)) + localProgress = Progress(totalUnitCount: Int64(inBuffer.freeCapacity)) // To report progress, we have to try reading in smaller chunks than the whole file. Aim for about 1% increments. - preferredChunkSize = max(length / 100, 1024 * 4) + preferredChunkSize = max(inBuffer.freeCapacity / 100, 1024 * 4) } else { localProgress = nil // Get it all in one go, if possible - preferredChunkSize = length + preferredChunkSize = inBuffer.freeCapacity } - var numBytesRemaining = length - while numBytesRemaining > 0 { + while !inBuffer.isFull { if let localProgress, localProgress.isCancelled { throw CocoaError(.userCancelled) } @@ -467,22 +473,27 @@ private func readBytesFromFileDescriptor(_ fd: Int32, path: PathOrURL, buffer in var numBytesRequested = CUnsignedInt(clamping: min(preferredChunkSize, Int(CInt.max))) // Furthermore, don't request more than the number of bytes remaining - if numBytesRequested > numBytesRemaining { - numBytesRequested = CUnsignedInt(clamping: min(numBytesRemaining, Int(CInt.max))) + if numBytesRequested > inBuffer.freeCapacity { + numBytesRequested = CUnsignedInt(clamping: min(inBuffer.freeCapacity, Int(CInt.max))) } - var numBytesRead: CInt + var numBytesRead: CInt = 0 repeat { if let localProgress, localProgress.isCancelled { throw CocoaError(.userCancelled) } // read takes an Int-sized argument, which will always be at least the size of Int32. + inBuffer.withUnsafeMutableBytes { buffer, initializedCount in #if os(Windows) - numBytesRead = _read(fd, buffer, numBytesRequested) + numBytesRead = _read(fd, buffer.baseAddress!.advanced(by: initializedCount), numBytesRequested) #else - numBytesRead = CInt(read(fd, buffer, Int(numBytesRequested))) + numBytesRead = CInt(read(fd, buffer.baseAddress!.advanced(by: initializedCount), Int(numBytesRequested))) #endif + if numBytesRead >= 0 { + initializedCount += Int(clamping: numBytesRead) + } + } } while numBytesRead < 0 && errno == EINTR if numBytesRead < 0 { @@ -495,21 +506,59 @@ private func readBytesFromFileDescriptor(_ fd: Int32, path: PathOrURL, buffer in break } else { // Partial read - numBytesRemaining -= Int(clamping: numBytesRead) - if numBytesRemaining < 0 { - // Just in case; we do not want to have a negative amount of bytes remaining. We will just assume that is the end. - numBytesRemaining = 0 - } - localProgress?.completedUnitCount = Int64(length - numBytesRemaining) + localProgress?.completedUnitCount = Int64(length - inBuffer.freeCapacity) // The `readUntilLength` argument controls if we should end early when `read` returns less than the amount requested, or if we should continue to loop until we have reached `length` bytes. if !readUntilLength && numBytesRead < numBytesRequested { break } + } + } +} - buffer = buffer.advanced(by: numericCast(numBytesRead)) +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { +#if FOUNDATION_FRAMEWORK + public typealias ReadingOptions = NSData.ReadingOptions +#else + public struct ReadingOptions : OptionSet, Sendable { + public let rawValue: UInt + public init(rawValue: UInt) { self.rawValue = rawValue } + + public static let mappedIfSafe = ReadingOptions(rawValue: 1 << 0) + public static let uncached = ReadingOptions(rawValue: 1 << 1) + public static let alwaysMapped = ReadingOptions(rawValue: 1 << 3) + } +#endif + +#if !FOUNDATION_FRAMEWORK + @_spi(SwiftCorelibsFoundation) + public dynamic init(_contentsOfRemote url: URL, options: ReadingOptions = []) throws { + assert(!url.isFileURL) + throw CocoaError(.fileReadUnsupportedScheme) + } +#endif + + /// Initialize a `Data` with the contents of a `URL`. + /// + /// - parameter url: The `URL` to read. + /// - parameter options: Options for the read operation. Default value is `[]`. + /// - throws: An error in the Cocoa domain, if `url` cannot be read. + public init(contentsOf url: __shared URL, options: ReadingOptions = []) throws { + if url.isFileURL { + self = try readDataFromFile(path: .url(url), reportProgress: true, options: options) + } else { +#if FOUNDATION_FRAMEWORK + // Fallback to NSData, to read via NSURLSession + let d = try NSData(contentsOf: url, options: NSData.ReadingOptions(rawValue: options.rawValue)) + self.init(referencing: d) +#else + try self.init(_contentsOfRemote: url, options: options) +#endif } } - return length - numBytesRemaining + internal init(contentsOfFile path: String, options: ReadingOptions = []) throws { + self = try readDataFromFile(path: .path(path), reportProgress: true, options: options) + } } diff --git a/Sources/FoundationEssentials/Data/Data+Searching.swift b/Sources/FoundationEssentials/Data/Data+Searching.swift new file mode 100644 index 000000000..5e0ffae00 --- /dev/null +++ b/Sources/FoundationEssentials/Data/Data+Searching.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { +#if FOUNDATION_FRAMEWORK + public typealias SearchOptions = NSData.SearchOptions + + /// Find the given `Data` in the content of this `Data`. + /// + /// - parameter dataToFind: The data to be searched for. + /// - parameter options: Options for the search. Default value is `[]`. + /// - parameter range: The range of this data in which to perform the search. Default value is `nil`, which means the entire content of this data. + /// - returns: A `Range` specifying the location of the found data, or nil if a match could not be found. + /// - precondition: `range` must be in the bounds of the Data. + public func range(of dataToFind: Data, options: Data.SearchOptions = [], in range: Range? = nil) -> Range? { + let nsRange : NSRange + if let r = range { + nsRange = NSRange(location: r.lowerBound - startIndex, length: r.upperBound - r.lowerBound) + } else { + nsRange = NSRange(location: 0, length: count) + } + let nsData = self as NSData + let opts = NSData.SearchOptions(rawValue: options.rawValue) + let result = nsData.range(of: dataToFind, options: opts, in: nsRange) + if result.location == NSNotFound { + return nil + } + return (result.location + startIndex)..<((result.location + startIndex) + result.length) + } +#else + // TODO: Implement range(of:options:in:) for Foundation package. + + public struct SearchOptions : OptionSet, Sendable { + public let rawValue: UInt + + public init(rawValue: UInt) { + self.rawValue = rawValue + } + /// Search from the end of the data object. + public static let backwards = SearchOptions(rawValue: 1 << 0) + /// Search is limited to start (or end, if searching backwards) of the data object. + public static let anchored = SearchOptions(rawValue: 1 << 1) + } +#endif +} diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index 6f51c3d16..354c5a254 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -56,9 +56,9 @@ private func openFileDescriptorProtected(path: UnsafePointer, flags: Int3 } #endif -private func writeToFileDescriptorWithProgress(_ fd: Int32, buffer: UnsafeRawBufferPointer, reportProgress: Bool) throws -> Int { +private func writeToFileDescriptorWithProgress(_ fd: Int32, buffer: RawSpan, reportProgress: Bool) throws -> Int { // Fetch this once - let length = buffer.count + let length = buffer.byteCount let preferredChunkSize: Int let localProgress: Progress? @@ -72,24 +72,22 @@ private func writeToFileDescriptorWithProgress(_ fd: Int32, buffer: UnsafeRawBuf localProgress = nil } - var nextRange = buffer.startIndex.. 0 { + var remaining = buffer + while !remaining.isEmpty { if let localProgress, localProgress.isCancelled { throw CocoaError(.userCancelled) } // Don't ever attempt to write more than (2GB - 1 byte). Some platforms will return an error over that amount. let numBytesRequested = CInt(clamping: min(preferredChunkSize, Int(CInt.max))) - let smallestAmountToRead = min(Int(numBytesRequested), numBytesRemaining) - let upperBound = nextRange.startIndex + smallestAmountToRead - nextRange = nextRange.startIndex..= 0 { _close(fd) } } - let callback = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.count)) : nil + let callback = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.byteCount)) : nil do { try write(buffer: buffer, toFileDescriptor: fd, path: inPath, parentProgress: callback) @@ -517,7 +503,7 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint defer { close(fd) } - let parentProgress = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.count)) : nil + let parentProgress = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.byteCount)) : nil do { try write(buffer: buffer, toFileDescriptor: fd, path: inPath, parentProgress: parentProgress) @@ -629,7 +615,7 @@ private func writeToFileAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoint } /// Create a new file out of `Data` at a path, not using atomic writing. -private func writeToFileNoAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPointer, options: Data.WritingOptions, attributes: [String : Data], reportProgress: Bool) throws { +private func writeToFileNoAux(path inPath: PathOrURL, buffer: RawSpan, options: Data.WritingOptions, attributes: [String : Data], reportProgress: Bool) throws { #if !os(WASI) // `.atomic` is unavailable on WASI assert(!options.contains(.atomic)) #endif @@ -646,7 +632,7 @@ private func writeToFileNoAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoi } defer { _close(fd) } - let callback: Progress? = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.count)) : nil + let callback: Progress? = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.byteCount)) : nil do { try write(buffer: buffer, toFileDescriptor: fd, path: inPath, parentProgress: callback) @@ -681,7 +667,7 @@ private func writeToFileNoAux(path inPath: PathOrURL, buffer: UnsafeRawBufferPoi defer { close(fd) } - let parentProgress = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.count)) : nil + let parentProgress = (reportProgress && Progress.current() != nil) ? Progress(totalUnitCount: Int64(buffer.byteCount)) : nil do { try write(buffer: buffer, toFileDescriptor: fd, path: inPath, parentProgress: parentProgress) @@ -720,5 +706,65 @@ private func writeExtendedAttributes(fd: Int32, attributes: [String : Data]) { } } } - #endif // !NO_FILESYSTEM + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { +#if FOUNDATION_FRAMEWORK + public typealias WritingOptions = NSData.WritingOptions +#else + + // This is imported from the ObjC 'option set', which is actually a combination of an option and an enumeration (file protection). + public struct WritingOptions : OptionSet, Sendable { + public let rawValue: UInt + public init(rawValue: UInt) { self.rawValue = rawValue } + + /// An option to write data to an auxiliary file first and then replace the original file with the auxiliary file when the write completes. +#if os(WASI) + @available(*, unavailable, message: "atomic writing is unavailable in WASI because temporary files are not supported") +#endif + public static let atomic = WritingOptions(rawValue: 1 << 0) + + /// An option that attempts to write data to a file and fails with an error if the destination file already exists. + public static let withoutOverwriting = WritingOptions(rawValue: 1 << 1) + + /// An option to not encrypt the file when writing it out. + public static let noFileProtection = WritingOptions(rawValue: 0x10000000) + + /// An option to make the file accessible only while the device is unlocked. + public static let completeFileProtection = WritingOptions(rawValue: 0x20000000) + + /// An option to allow the file to be accessible while the device is unlocked or the file is already open. + public static let completeFileProtectionUnlessOpen = WritingOptions(rawValue: 0x30000000) + + /// An option to allow the file to be accessible after a user first unlocks the device. + public static let completeFileProtectionUntilFirstUserAuthentication = WritingOptions(rawValue: 0x40000000) + + /// An option the system uses when determining the file protection options that the system assigns to the data. + public static let fileProtectionMask = WritingOptions(rawValue: 0xf0000000) + } +#endif + + /// Write the contents of the `Data` to a location. + /// + /// - parameter url: The location to write the data into. + /// - parameter options: Options for writing the data. Default value is `[]`. + /// - throws: An error in the Cocoa domain, if there is an error writing to the `URL`. + public func write(to url: URL, options: Data.WritingOptions = []) throws { +#if !os(WASI) // `.atomic` is unavailable on WASI + if options.contains(.withoutOverwriting) && options.contains(.atomic) { + fatalError("withoutOverwriting is not supported with atomic") + } +#endif + + guard url.isFileURL else { + throw CocoaError(.fileWriteUnsupportedScheme) + } + +#if !NO_FILESYSTEM + try writeToFile(path: .url(url), buffer: self.bytes, options: options, reportProgress: true) +#else + throw CocoaError(.featureUnsupported) +#endif + } +} diff --git a/Sources/FoundationEssentials/Data/Data.swift b/Sources/FoundationEssentials/Data/Data.swift index db43f8d5d..e1cead6ae 100644 --- a/Sources/FoundationEssentials/Data/Data.swift +++ b/Sources/FoundationEssentials/Data/Data.swift @@ -106,1780 +106,27 @@ internal func _withStackOrHeapBuffer(capacity: Int, _ body: (UnsafeMutableBuffer body(UnsafeMutableBufferPointer(start: nil, count: 0)) return } - typealias InlineBuffer = ( // 32 bytes - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8 - ) - let inlineCount = MemoryLayout.size - if capacity <= inlineCount { - var buffer: InlineBuffer = ( - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 - ) - withUnsafeMutableBytes(of: &buffer) { buffer in - assert(buffer.count == inlineCount) - buffer.withMemoryRebound(to: UInt8.self) { - body(UnsafeMutableBufferPointer(start: $0.baseAddress, count: capacity)) - } - } - return - } - - let buffer = UnsafeMutableBufferPointer.allocate(capacity: capacity) - defer { buffer.deallocate() } - body(buffer) -} - -// Underlying storage representation for medium and large data. -// Inlinability strategy: methods from here should not inline into InlineSlice or LargeSlice unless trivial. -// NOTE: older overlays called this class _DataStorage. The two must -// coexist without a conflicting ObjC class name, so it was renamed. -// The old name must not be used in the new runtime. -@usableFromInline -@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) -internal final class __DataStorage : @unchecked Sendable { - @usableFromInline static let maxSize = Int.max >> 1 - @usableFromInline static let vmOpsThreshold = Platform.pageSize * 4 - -#if !FOUNDATION_FRAMEWORK - static func allocate(_ size: Int, _ clear: Bool) -> UnsafeMutableRawPointer? { - if clear { - return calloc(1, size) - } else { - return malloc(size) - } - } - - static func reallocate(_ ptr: UnsafeMutableRawPointer, _ newSize: Int) -> UnsafeMutableRawPointer? { - return realloc(ptr, newSize); - } -#endif // !FOUNDATION_FRAMEWORK - - @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. - static func move(_ dest_: UnsafeMutableRawPointer, _ source_: UnsafeRawPointer?, _ num_: Int) { - var dest = dest_ - var source = source_ - var num = num_ - if __DataStorage.vmOpsThreshold <= num && ((unsafeBitCast(source, to: Int.self) | Int(bitPattern: dest)) & (Platform.pageSize - 1)) == 0 { - let pages = Platform.roundDownToMultipleOfPageSize(num) - Platform.copyMemoryPages(source!, dest, pages) - source = source!.advanced(by: pages) - dest = dest.advanced(by: pages) - num -= pages - } - if num > 0 { - memmove(dest, source!, num) - } - } - - @inlinable // This is @inlinable as trivially forwarding, and does not escape the _DataStorage boundary layer. - static func shouldAllocateCleared(_ size: Int) -> Bool { - return (size > (128 * 1024)) - } - - @usableFromInline var _bytes: UnsafeMutableRawPointer? - @usableFromInline var _length: Int - @usableFromInline var _capacity: Int - @usableFromInline var _offset: Int - @usableFromInline var _deallocator: ((UnsafeMutableRawPointer, Int) -> Void)? - @usableFromInline var _needToZero: Bool - - @inlinable // This is @inlinable as trivially computable. - var bytes: UnsafeRawPointer? { - return UnsafeRawPointer(_bytes)?.advanced(by: -_offset) - } - - @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is generic and trivially forwarding. - @discardableResult - func withUnsafeBytes(in range: Range, apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { - return try apply(UnsafeRawBufferPointer(start: _bytes?.advanced(by: range.lowerBound - _offset), count: Swift.min(range.upperBound - range.lowerBound, _length))) - } - - @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is generic and trivially forwarding. - @discardableResult - func withUnsafeMutableBytes(in range: Range, apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { - return try apply(UnsafeMutableRawBufferPointer(start: _bytes!.advanced(by:range.lowerBound - _offset), count: Swift.min(range.upperBound - range.lowerBound, _length))) - } - - @inlinable // This is @inlinable as trivially computable. - var mutableBytes: UnsafeMutableRawPointer? { - return _bytes?.advanced(by: -_offset) - } - - @inlinable - static var copyWillRetainMask: Int { -#if _pointerBitWidth(_64) - return Int(bitPattern: 0x8000000000000000) -#elseif _pointerBitWidth(_32) - return Int(bitPattern: 0x80000000) -#endif - } - - @inlinable - static var capacityMask: Int { -#if _pointerBitWidth(_64) - return Int(bitPattern: 0x7FFFFFFFFFFFFFFF) -#elseif _pointerBitWidth(_32) - return Int(bitPattern: 0x7FFFFFFF) -#endif - } - - @inlinable // This is @inlinable as trivially computable. - var capacity: Int { - return _capacity & __DataStorage.capacityMask - } - - @inlinable - var _copyWillRetain: Bool { - get { - return _capacity & __DataStorage.copyWillRetainMask == 0 - } - set { - if !newValue { - _capacity |= __DataStorage.copyWillRetainMask - } else { - _capacity &= __DataStorage.capacityMask - } - } - } - - @inlinable // This is @inlinable as trivially computable. - var length: Int { - get { - return _length - } - set { - setLength(newValue) - } - } - - @inlinable // This is inlinable as trivially computable. - var isExternallyOwned: Bool { - // all __DataStorages will have some sort of capacity, because empty cases hit the .empty enum _Representation - // anything with 0 capacity means that we have not allocated this pointer and consequently mutation is not ours to make. - return _capacity == 0 - } - - @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. - func ensureUniqueBufferReference(growingTo newLength: Int = 0, clear: Bool = false) { - guard isExternallyOwned || newLength > _capacity else { return } - - if newLength == 0 { - if isExternallyOwned { - let newCapacity = malloc_good_size(_length) - let newBytes = __DataStorage.allocate(newCapacity, false) - __DataStorage.move(newBytes!, _bytes!, _length) - _freeBytes() - _bytes = newBytes - _capacity = newCapacity - _needToZero = false - } - } else if isExternallyOwned { - let newCapacity = malloc_good_size(newLength) - let newBytes = __DataStorage.allocate(newCapacity, clear) - if let bytes = _bytes { - __DataStorage.move(newBytes!, bytes, _length) - } - _freeBytes() - _bytes = newBytes - _capacity = newCapacity - _length = newLength - _needToZero = true - } else { - let cap = _capacity - var additionalCapacity = (newLength >> (__DataStorage.vmOpsThreshold <= newLength ? 2 : 1)) - if Int.max - additionalCapacity < newLength { - additionalCapacity = 0 - } - var newCapacity = malloc_good_size(Swift.max(cap, newLength + additionalCapacity)) - let origLength = _length - var allocateCleared = clear && __DataStorage.shouldAllocateCleared(newCapacity) - var newBytes: UnsafeMutableRawPointer? = nil - if _bytes == nil { - newBytes = __DataStorage.allocate(newCapacity, allocateCleared) - if newBytes == nil { - /* Try again with minimum length */ - allocateCleared = clear && __DataStorage.shouldAllocateCleared(newLength) - newBytes = __DataStorage.allocate(newLength, allocateCleared) - } - } else { - let tryCalloc = (origLength == 0 || (newLength / origLength) >= 4) - if allocateCleared && tryCalloc { - newBytes = __DataStorage.allocate(newCapacity, true) - if let newBytes = newBytes { - __DataStorage.move(newBytes, _bytes!, origLength) - _freeBytes() - } - } - /* Where calloc/memmove/free fails, realloc might succeed */ - if newBytes == nil { - allocateCleared = false - if _deallocator != nil { - newBytes = __DataStorage.allocate(newCapacity, true) - if let newBytes = newBytes { - __DataStorage.move(newBytes, _bytes!, origLength) - _freeBytes() - } - } else { - newBytes = __DataStorage.reallocate(_bytes!, newCapacity) - } - } - /* Try again with minimum length */ - if newBytes == nil { - newCapacity = malloc_good_size(newLength) - allocateCleared = clear && __DataStorage.shouldAllocateCleared(newCapacity) - if allocateCleared && tryCalloc { - newBytes = __DataStorage.allocate(newCapacity, true) - if let newBytes = newBytes { - __DataStorage.move(newBytes, _bytes!, origLength) - _freeBytes() - } - } - if newBytes == nil { - allocateCleared = false - newBytes = __DataStorage.reallocate(_bytes!, newCapacity) - } - } - } - - if newBytes == nil { - /* Could not allocate bytes */ - // At this point if the allocation cannot occur the process is likely out of memory - // and Bad-Thingsā„¢ are going to happen anyhow - fatalError("unable to allocate memory for length (\(newLength))") - } - - if origLength < newLength && clear && !allocateCleared { - _ = memset(newBytes!.advanced(by: origLength), 0, newLength - origLength) - } - - /* _length set by caller */ - _bytes = newBytes - _capacity = newCapacity - /* Realloc/memset doesn't zero out the entire capacity, so we must be safe and clear next time we grow the length */ - _needToZero = !allocateCleared - } - } - - @inlinable // This is @inlinable as it does not escape the _DataStorage boundary layer. - func _freeBytes() { - if let bytes = _bytes { - if let dealloc = _deallocator { - dealloc(bytes, length) - } else { - free(bytes) - } - } - _deallocator = nil - } - - @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is trivially computed. - func enumerateBytes(in range: Range, _ block: (_ buffer: UnsafeBufferPointer, _ byteIndex: Data.Index, _ stop: inout Bool) -> Void) { - var stopv: Bool = false - let buffer = UnsafeRawBufferPointer(start: _bytes, count: Swift.min(range.upperBound - range.lowerBound, _length)) - buffer.withMemoryRebound(to: UInt8.self) { block($0, 0, &stopv) } - } - - @inlinable // This is @inlinable as it does not escape the _DataStorage boundary layer. - func setLength(_ length: Int) { - let origLength = _length - let newLength = length - if capacity < newLength || _bytes == nil { - ensureUniqueBufferReference(growingTo: newLength, clear: true) - } else if origLength < newLength && _needToZero { - _ = memset(_bytes! + origLength, 0, newLength - origLength) - } else if newLength < origLength { - _needToZero = true - } - _length = newLength - } - - @inlinable // This is @inlinable as it does not escape the _DataStorage boundary layer. - func append(_ bytes: UnsafeRawPointer, length: Int) { - precondition(length >= 0, "Length of appending bytes must not be negative") - let origLength = _length - let newLength = origLength + length - if capacity < newLength || _bytes == nil { - ensureUniqueBufferReference(growingTo: newLength, clear: false) - } - _length = newLength - __DataStorage.move(_bytes!.advanced(by: origLength), bytes, length) - } - - @inlinable // This is @inlinable despite escaping the __DataStorage boundary layer because it is trivially computed. - func get(_ index: Int) -> UInt8 { - // index must have already been validated by the caller - return _bytes!.load(fromByteOffset: index - _offset, as: UInt8.self) - } - - @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is trivially computed. - func set(_ index: Int, to value: UInt8) { - // index must have already been validated by the caller - ensureUniqueBufferReference() - _bytes!.storeBytes(of: value, toByteOffset: index - _offset, as: UInt8.self) - } - - @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is trivially computed. - func copyBytes(to pointer: UnsafeMutableRawPointer, from range: Range) { - let offsetPointer = UnsafeRawBufferPointer(start: _bytes?.advanced(by: range.lowerBound - _offset), count: Swift.min(range.upperBound - range.lowerBound, _length)) - UnsafeMutableRawBufferPointer(start: pointer, count: range.upperBound - range.lowerBound).copyMemory(from: offsetPointer) - } - - #if FOUNDATION_FRAMEWORK - @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) - #endif - @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. - func replaceBytes(in range_: Range, with replacementBytes: UnsafeRawPointer?, length replacementLength: Int) { - let range = range_.lowerBound - _offset ..< range_.upperBound - _offset - let currentLength = _length - let resultingLength = currentLength - (range.upperBound - range.lowerBound) + replacementLength - let shift = resultingLength - currentLength - let mutableBytes: UnsafeMutableRawPointer - if resultingLength > currentLength { - ensureUniqueBufferReference(growingTo: resultingLength) - _length = resultingLength - } else { - ensureUniqueBufferReference() - } - mutableBytes = _bytes! - /* shift the trailing bytes */ - let start = range.lowerBound - let length = range.upperBound - range.lowerBound - if shift != 0 { - memmove(mutableBytes + start + replacementLength, mutableBytes + start + length, currentLength - start - length) - } - if replacementLength != 0 { - if let replacementBytes = replacementBytes { - memmove(mutableBytes + start, replacementBytes, replacementLength) - } else { - _ = memset(mutableBytes + start, 0, replacementLength) - } - } - - if resultingLength < currentLength { - setLength(resultingLength) - } - } - - @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. - func resetBytes(in range_: Range) { - let range = range_.lowerBound - _offset ..< range_.upperBound - _offset - if range.upperBound - range.lowerBound == 0 { return } - if _length < range.upperBound { - if capacity <= range.upperBound { - ensureUniqueBufferReference(growingTo: range.upperBound, clear: false) - } - _length = range.upperBound - } else { - ensureUniqueBufferReference() - } - _ = memset(_bytes!.advanced(by: range.lowerBound), 0, range.upperBound - range.lowerBound) - } - - @usableFromInline // This is not @inlinable as a non-trivial, non-convenience initializer. - init(length: Int) { - precondition(length < __DataStorage.maxSize) - var capacity = (length < 1024 * 1024 * 1024) ? length + (length >> 2) : length - if __DataStorage.vmOpsThreshold <= capacity { - capacity = Platform.roundUpToMultipleOfPageSize(capacity) - } - - let clear = __DataStorage.shouldAllocateCleared(length) - _bytes = __DataStorage.allocate(capacity, clear)! - _capacity = capacity - _needToZero = !clear - _length = 0 - _offset = 0 - setLength(length) - } - - @usableFromInline // This is not @inlinable as a non-convenience initializer. - init(capacity capacity_: Int = 0) { - var capacity = capacity_ - precondition(capacity < __DataStorage.maxSize) - if __DataStorage.vmOpsThreshold <= capacity { - capacity = Platform.roundUpToMultipleOfPageSize(capacity) - } - _length = 0 - _bytes = __DataStorage.allocate(capacity, false)! - _capacity = capacity - _needToZero = true - _offset = 0 - } - - @usableFromInline // This is not @inlinable as a non-convenience initializer. - init(bytes: UnsafeRawPointer?, length: Int) { - precondition(length < __DataStorage.maxSize) - _offset = 0 - if length == 0 { - _capacity = 0 - _length = 0 - _needToZero = false - _bytes = nil - } else if __DataStorage.vmOpsThreshold <= length { - _capacity = length - _length = length - _needToZero = true - _bytes = __DataStorage.allocate(length, false)! - __DataStorage.move(_bytes!, bytes, length) - } else { - var capacity = length - if __DataStorage.vmOpsThreshold <= capacity { - capacity = Platform.roundUpToMultipleOfPageSize(capacity) - } - _length = length - _bytes = __DataStorage.allocate(capacity, false)! - _capacity = capacity - _needToZero = true - __DataStorage.move(_bytes!, bytes, length) - } - } - - @usableFromInline // This is not @inlinable as a non-convenience initializer. - init(bytes: UnsafeMutableRawPointer?, length: Int, copy: Bool, deallocator: ((UnsafeMutableRawPointer, Int) -> Void)?, offset: Int) { - precondition(length < __DataStorage.maxSize) - _offset = offset - if length == 0 { - _capacity = 0 - _length = 0 - _needToZero = false - _bytes = nil - if let dealloc = deallocator, - let bytes_ = bytes { - dealloc(bytes_, length) - } - } else if !copy { - _capacity = length - _length = length - _needToZero = false - _bytes = bytes - _deallocator = deallocator - } else if __DataStorage.vmOpsThreshold <= length { - _capacity = length - _length = length - _needToZero = true - _bytes = __DataStorage.allocate(length, false)! - __DataStorage.move(_bytes!, bytes, length) - if let dealloc = deallocator { - dealloc(bytes!, length) - } - } else { - var capacity = length - if __DataStorage.vmOpsThreshold <= capacity { - capacity = Platform.roundUpToMultipleOfPageSize(capacity) - } - _length = length - _bytes = __DataStorage.allocate(capacity, false)! - _capacity = capacity - _needToZero = true - __DataStorage.move(_bytes!, bytes, length) - if let dealloc = deallocator { - dealloc(bytes!, length) - } - } - } - - @usableFromInline - init(offset: Int, bytes: UnsafeMutableRawPointer, capacity: Int, needToZero: Bool, length: Int, deallocator: ((UnsafeMutableRawPointer, Int) -> Void)?) { - _offset = offset - _bytes = bytes - _capacity = capacity - _needToZero = needToZero - _length = length - _deallocator = deallocator - } - - deinit { - _freeBytes() - } - - @inlinable // This is @inlinable despite escaping the __DataStorage boundary layer because it is trivially computed. - func mutableCopy(_ range: Range) -> __DataStorage { - return __DataStorage(bytes: _bytes?.advanced(by: range.lowerBound - _offset), length: range.upperBound - range.lowerBound, copy: true, deallocator: nil, offset: range.lowerBound) - } -} - -@frozen -@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) -#if compiler(>=6.2) -@_addressableForDependencies -#endif -public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollection, RangeReplaceableCollection, MutableDataProtocol, ContiguousBytes, Sendable { - - public typealias Index = Int - public typealias Indices = Range - - // A small inline buffer of bytes suitable for stack-allocation of small data. - // Inlinability strategy: everything here should be inlined for direct operation on the stack wherever possible. - @usableFromInline - @frozen - internal struct InlineData : Sendable { -#if _pointerBitWidth(_64) - @usableFromInline typealias Buffer = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) //len //enum - @usableFromInline var bytes: Buffer -#elseif _pointerBitWidth(_32) - @usableFromInline typealias Buffer = (UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8) //len //enum - @usableFromInline var bytes: Buffer -#else - #error ("Unsupported architecture: a definition of Buffer needs to be made with N = (MemoryLayout<(Int, Int)>.size - 2) UInt8 members to a tuple") -#endif - @usableFromInline var length: UInt8 - - @inlinable // This is @inlinable as trivially computable. - static func canStore(count: Int) -> Bool { - return count <= MemoryLayout.size - } - - static var maximumCapacity: Int { - return MemoryLayout.size - } - - @inlinable // This is @inlinable as a convenience initializer. - init(_ srcBuffer: UnsafeRawBufferPointer) { - self.init(count: srcBuffer.count) - if !srcBuffer.isEmpty { - Swift.withUnsafeMutableBytes(of: &bytes) { dstBuffer in - dstBuffer.baseAddress?.copyMemory(from: srcBuffer.baseAddress!, byteCount: srcBuffer.count) - } - } - } - - @inlinable // This is @inlinable as a trivial initializer. - init(count: Int = 0) { - assert(count <= MemoryLayout.size) -#if _pointerBitWidth(_64) - bytes = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0)) -#elseif _pointerBitWidth(_32) - bytes = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0)) -#else - #error ("Unsupported architecture: initialization for Buffer is required for this architecture") -#endif - length = UInt8(count) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(_ slice: InlineSlice, count: Int) { - self.init(count: count) - Swift.withUnsafeMutableBytes(of: &bytes) { dstBuffer in - slice.withUnsafeBytes { srcBuffer in - dstBuffer.copyMemory(from: UnsafeRawBufferPointer(start: srcBuffer.baseAddress, count: count)) - } - } - } - - @inlinable // This is @inlinable as a convenience initializer. - init(_ slice: LargeSlice, count: Int) { - self.init(count: count) - Swift.withUnsafeMutableBytes(of: &bytes) { dstBuffer in - slice.withUnsafeBytes { srcBuffer in - dstBuffer.copyMemory(from: UnsafeRawBufferPointer(start: srcBuffer.baseAddress, count: count)) - } - } - } - - @inlinable // This is @inlinable as trivially computable. - var capacity: Int { - return MemoryLayout.size - } - - @inlinable // This is @inlinable as trivially computable. - var count: Int { - get { - return Int(length) - } - set(newValue) { - assert(newValue <= MemoryLayout.size) - if newValue > length { - resetBytes(in: Int(length) ..< newValue) // Also extends length - } else { - length = UInt8(newValue) - } - } - } - - @inlinable // This is @inlinable as trivially computable. - var startIndex: Int { - return 0 - } - - @inlinable // This is @inlinable as trivially computable. - var endIndex: Int { - return count - } - - @inlinable // This is @inlinable as a generic, trivially forwarding function. - func withUnsafeBytes(_ apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { - let count = Int(length) - return try Swift.withUnsafeBytes(of: bytes) { (rawBuffer) throws -> Result in - return try apply(UnsafeRawBufferPointer(start: rawBuffer.baseAddress, count: count)) - } - } - - @inlinable // This is @inlinable as a generic, trivially forwarding function. - mutating func withUnsafeMutableBytes(_ apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { - let count = Int(length) - return try Swift.withUnsafeMutableBytes(of: &bytes) { (rawBuffer) throws -> Result in - return try apply(UnsafeMutableRawBufferPointer(start: rawBuffer.baseAddress, count: count)) - } - } - - @inlinable // This is @inlinable as trivially computable. - mutating func append(byte: UInt8) { - let count = self.count - assert(count + 1 <= MemoryLayout.size) - Swift.withUnsafeMutableBytes(of: &bytes) { $0[count] = byte } - self.length += 1 - } - - @inlinable // This is @inlinable as trivially computable. - mutating func append(contentsOf buffer: UnsafeRawBufferPointer) { - guard !buffer.isEmpty else { return } - assert(count + buffer.count <= MemoryLayout.size) - let cnt = count - _ = Swift.withUnsafeMutableBytes(of: &bytes) { rawBuffer in - rawBuffer.baseAddress?.advanced(by: cnt).copyMemory(from: buffer.baseAddress!, byteCount: buffer.count) - } - - length += UInt8(buffer.count) - } - - @inlinable // This is @inlinable as trivially computable. - subscript(index: Index) -> UInt8 { - get { - assert(index <= MemoryLayout.size) - precondition(index < length, "index \(index) is out of bounds of 0..<\(length)") - return Swift.withUnsafeBytes(of: bytes) { rawBuffer -> UInt8 in - return rawBuffer[index] - } - } - set(newValue) { - assert(index <= MemoryLayout.size) - precondition(index < length, "index \(index) is out of bounds of 0..<\(length)") - Swift.withUnsafeMutableBytes(of: &bytes) { rawBuffer in - rawBuffer[index] = newValue - } - } - } - - @inlinable // This is @inlinable as trivially computable. - mutating func resetBytes(in range: Range) { - assert(range.lowerBound <= MemoryLayout.size) - assert(range.upperBound <= MemoryLayout.size) - precondition(range.lowerBound <= length, "index \(range.lowerBound) is out of bounds of 0..<\(length)") - if length < range.upperBound { - length = UInt8(range.upperBound) - } - - let _ = Swift.withUnsafeMutableBytes(of: &bytes) { rawBuffer in - memset(rawBuffer.baseAddress!.advanced(by: range.lowerBound), 0, range.upperBound - range.lowerBound) - } - } - - @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. - mutating func replaceSubrange(_ subrange: Range, with replacementBytes: UnsafeRawPointer?, count replacementLength: Int) { - assert(subrange.lowerBound <= MemoryLayout.size) - assert(subrange.upperBound <= MemoryLayout.size) - assert(count - (subrange.upperBound - subrange.lowerBound) + replacementLength <= MemoryLayout.size) - precondition(subrange.lowerBound <= length, "index \(subrange.lowerBound) is out of bounds of 0..<\(length)") - precondition(subrange.upperBound <= length, "index \(subrange.upperBound) is out of bounds of 0..<\(length)") - let currentLength = count - let resultingLength = currentLength - (subrange.upperBound - subrange.lowerBound) + replacementLength - let shift = resultingLength - currentLength - Swift.withUnsafeMutableBytes(of: &bytes) { mutableBytes in - /* shift the trailing bytes */ - let start = subrange.lowerBound - let length = subrange.upperBound - subrange.lowerBound - if shift != 0 { - memmove(mutableBytes.baseAddress!.advanced(by: start + replacementLength), mutableBytes.baseAddress!.advanced(by: start + length), currentLength - start - length) - } - if replacementLength != 0 { - memmove(mutableBytes.baseAddress!.advanced(by: start), replacementBytes!, replacementLength) - } - } - length = UInt8(resultingLength) - } - - @inlinable // This is @inlinable as trivially computable. - func copyBytes(to pointer: UnsafeMutableRawPointer, from range: Range) { - precondition(startIndex <= range.lowerBound, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(startIndex <= range.upperBound, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(range.upperBound <= endIndex, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - - Swift.withUnsafeBytes(of: bytes) { - let cnt = Swift.min($0.count, range.upperBound - range.lowerBound) - guard cnt > 0 else { return } - pointer.copyMemory(from: $0.baseAddress!.advanced(by: range.lowerBound), byteCount: cnt) - } - } - - @inline(__always) // This should always be inlined into _Representation.hash(into:). - func hash(into hasher: inout Hasher) { - // **NOTE**: this uses `count` (an Int) and NOT `length` (a UInt8) - // Despite having the same value, they hash differently. InlineSlice and LargeSlice both use `count` (an Int); if you combine the same bytes but with `length` over `count`, you can get a different hash. - // - // This affects slices, which are InlineSlice and not InlineData: - // - // let d = Data([0xFF, 0xFF]) // InlineData - // let s = Data([0, 0xFF, 0xFF]).dropFirst() // InlineSlice - // assert(s == d) - // assert(s.hashValue == d.hashValue) - hasher.combine(count) - - Swift.withUnsafeBytes(of: bytes) { - // We have access to the full byte buffer here, but not all of it is meaningfully used (bytes past self.length may be garbage). - let bytes = UnsafeRawBufferPointer(start: $0.baseAddress, count: self.count) - hasher.combine(bytes: bytes) - } - } - } - -#if _pointerBitWidth(_64) - @usableFromInline internal typealias HalfInt = Int32 -#elseif _pointerBitWidth(_32) - @usableFromInline internal typealias HalfInt = Int16 -#else - #error ("Unsupported architecture: a definition of half of the pointer sized Int needs to be defined for this architecture") -#endif - - // A buffer of bytes too large to fit in an InlineData, but still small enough to fit a storage pointer + range in two words. - // Inlinability strategy: everything here should be easily inlinable as large _DataStorage methods should not inline into here. - @usableFromInline - @frozen - internal struct InlineSlice : Sendable { - // ***WARNING*** - // These ivars are specifically laid out so that they cause the enum _Representation to be 16 bytes on 64 bit platforms. This means we _MUST_ have the class type thing last - @usableFromInline var slice: Range - @usableFromInline var storage: __DataStorage - - @inlinable // This is @inlinable as trivially computable. - static func canStore(count: Int) -> Bool { - return count < HalfInt.max - } - - @inlinable // This is @inlinable as a convenience initializer. - init(_ buffer: UnsafeRawBufferPointer) { - assert(buffer.count < HalfInt.max) - self.init(__DataStorage(bytes: buffer.baseAddress, length: buffer.count), count: buffer.count) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(capacity: Int) { - assert(capacity < HalfInt.max) - self.init(__DataStorage(capacity: capacity), count: 0) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(count: Int) { - assert(count < HalfInt.max) - self.init(__DataStorage(length: count), count: count) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(_ inline: InlineData) { - assert(inline.count < HalfInt.max) - self.init(inline.withUnsafeBytes { return __DataStorage(bytes: $0.baseAddress, length: $0.count) }, count: inline.count) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(_ inline: InlineData, range: Range) { - assert(range.lowerBound < HalfInt.max) - assert(range.upperBound < HalfInt.max) - self.init(inline.withUnsafeBytes { return __DataStorage(bytes: $0.baseAddress, length: $0.count) }, range: range) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(_ large: LargeSlice) { - assert(large.range.lowerBound < HalfInt.max) - assert(large.range.upperBound < HalfInt.max) - self.init(large.storage, range: large.range) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(_ large: LargeSlice, range: Range) { - assert(range.lowerBound < HalfInt.max) - assert(range.upperBound < HalfInt.max) - self.init(large.storage, range: range) - } - - @inlinable // This is @inlinable as a trivial initializer. - init(_ storage: __DataStorage, count: Int) { - assert(count < HalfInt.max) - self.storage = storage - slice = 0..) { - assert(range.lowerBound < HalfInt.max) - assert(range.upperBound < HalfInt.max) - self.storage = storage - slice = HalfInt(range.lowerBound).. 0 { - let additionalRange = Int(slice.upperBound) ..< Int(slice.upperBound) + difference - storage.resetBytes(in: additionalRange) // Also extends storage length - } else { - storage.length += difference - } - slice = slice.lowerBound..<(slice.lowerBound + HalfInt(newValue)) - } - } - - @inlinable // This is @inlinable as trivially computable. - var range: Range { - get { - return Int(slice.lowerBound)..(_ apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { - return try storage.withUnsafeBytes(in: range, apply: apply) - } - - @inlinable // This is @inlinable as a generic, trivially forwarding function. - mutating func withUnsafeMutableBytes(_ apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { - ensureUniqueReference() - return try storage.withUnsafeMutableBytes(in: range, apply: apply) - } - - @inlinable // This is @inlinable as reasonably small. - mutating func append(contentsOf buffer: UnsafeRawBufferPointer) { - assert(endIndex + buffer.count < HalfInt.max) - ensureUniqueReference() - let upperbound = storage.length + storage._offset - #if FOUNDATION_FRAMEWORK - if #available(macOS 14, iOS 17, watchOS 10, tvOS 17, *) { - storage.replaceBytes( - in: range.upperBound ..< upperbound, - with: buffer.baseAddress, - length: buffer.count) - } else { - storage.replaceBytes( - in: NSRange( - location: range.upperBound, - length: storage.length - (range.upperBound - storage._offset)), - with: buffer.baseAddress, - length: buffer.count) - } - #else - storage.replaceBytes(in: range.upperBound ..< upperbound, with: buffer.baseAddress, length: buffer.count) - #endif - slice = slice.lowerBound.. UInt8 { - get { - assert(index < HalfInt.max) - precondition(startIndex <= index, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(index < endIndex, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") - return storage.get(index) - } - set(newValue) { - assert(index < HalfInt.max) - precondition(startIndex <= index, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(index < endIndex, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") - ensureUniqueReference() - storage.set(index, to: newValue) - } - } - - @inlinable // This is @inlinable as reasonably small. - mutating func resetBytes(in range: Range) { - assert(range.lowerBound < HalfInt.max) - assert(range.upperBound < HalfInt.max) - precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - ensureUniqueReference() - storage.resetBytes(in: range) - if slice.upperBound < range.upperBound { - slice = slice.lowerBound.., with bytes: UnsafeRawPointer?, count cnt: Int) { - precondition(startIndex <= subrange.lowerBound, "index \(subrange.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(subrange.lowerBound <= endIndex, "index \(subrange.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(startIndex <= subrange.upperBound, "index \(subrange.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(subrange.upperBound <= endIndex, "index \(subrange.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - - ensureUniqueReference() - let upper = range.upperBound - #if FOUNDATION_FRAMEWORK - if #available(macOS 14, iOS 17, watchOS 10, tvOS 17, *) { - storage.replaceBytes(in: subrange, with: bytes, length: cnt) - } else { - let nsRange = NSRange( - location: subrange.lowerBound, - length: subrange.upperBound - subrange.lowerBound) - storage.replaceBytes(in: nsRange, with: bytes, length: cnt) - } - #else - storage.replaceBytes(in: subrange, with: bytes, length: cnt) - #endif - let resultingUpper = upper - (subrange.upperBound - subrange.lowerBound) + cnt - slice = slice.lowerBound..) { - precondition(startIndex <= range.lowerBound, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(startIndex <= range.upperBound, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(range.upperBound <= endIndex, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - storage.copyBytes(to: pointer, from: range) - } - - @inline(__always) // This should always be inlined into _Representation.hash(into:). - func hash(into hasher: inout Hasher) { - hasher.combine(count) - - // At most, hash the first 80 bytes of this data. - let range = startIndex ..< Swift.min(startIndex + 80, endIndex) - storage.withUnsafeBytes(in: range) { - hasher.combine(bytes: $0) - } - } - } - - // A reference wrapper around a Range for when the range of a data buffer is too large to whole in a single word. - // Inlinability strategy: everything should be inlinable as trivial. - @usableFromInline - @_fixed_layout - internal final class RangeReference : @unchecked Sendable { - @usableFromInline var range: Range - - @inlinable @inline(__always) // This is @inlinable as trivially forwarding. - var lowerBound: Int { - return range.lowerBound - } - - @inlinable @inline(__always) // This is @inlinable as trivially forwarding. - var upperBound: Int { - return range.upperBound - } - - @inlinable @inline(__always) // This is @inlinable as trivially computable. - var count: Int { - return range.upperBound - range.lowerBound - } - - @inlinable @inline(__always) // This is @inlinable as a trivial initializer. - init(_ range: Range) { - self.range = range - } - } - - // A buffer of bytes whose range is too large to fit in a single word. Used alongside a RangeReference to make it fit into _Representation's two-word size. - // Inlinability strategy: everything here should be easily inlinable as large _DataStorage methods should not inline into here. - @usableFromInline - @frozen - internal struct LargeSlice : Sendable { - // ***WARNING*** - // These ivars are specifically laid out so that they cause the enum _Representation to be 16 bytes on 64 bit platforms. This means we _MUST_ have the class type thing last - @usableFromInline var slice: RangeReference - @usableFromInline var storage: __DataStorage - - @inlinable // This is @inlinable as a convenience initializer. - init(_ buffer: UnsafeRawBufferPointer) { - self.init(__DataStorage(bytes: buffer.baseAddress, length: buffer.count), count: buffer.count) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(capacity: Int) { - self.init(__DataStorage(capacity: capacity), count: 0) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(count: Int) { - self.init(__DataStorage(length: count), count: count) - } - - @inlinable // This is @inlinable as a convenience initializer. - init(_ inline: InlineData) { - let storage = inline.withUnsafeBytes { return __DataStorage(bytes: $0.baseAddress, length: $0.count) } - self.init(storage, count: inline.count) - } - - @inlinable // This is @inlinable as a trivial initializer. - init(_ slice: InlineSlice) { - self.storage = slice.storage - self.slice = RangeReference(slice.range) - } - - @inlinable // This is @inlinable as a trivial initializer. - init(_ storage: __DataStorage, count: Int) { - self.storage = storage - self.slice = RangeReference(0..) { - self.storage = storage - self.slice = RangeReference(range) - } - - @inlinable // This is @inlinable as trivially computable (and inlining may help avoid retain-release traffic). - mutating func ensureUniqueReference() { - if !isKnownUniquelyReferenced(&storage) { - storage = storage.mutableCopy(range) - } - if !isKnownUniquelyReferenced(&slice) { - slice = RangeReference(range) - } - } - - @inlinable // This is @inlinable as trivially forwarding. - var startIndex: Int { - return slice.range.lowerBound - } - - @inlinable // This is @inlinable as trivially forwarding. - var endIndex: Int { - return slice.range.upperBound - } - - @inlinable // This is @inlinable as trivially forwarding. - var capacity: Int { - return storage.capacity - } - - @inlinable // This is @inlinable as trivially computable. - mutating func reserveCapacity(_ minimumCapacity: Int) { - ensureUniqueReference() - // the current capacity can be zero (representing externally owned buffer), and count can be greater than the capacity - storage.ensureUniqueBufferReference(growingTo: Swift.max(minimumCapacity, count)) - } - - @inlinable // This is @inlinable as trivially computable. - var count: Int { - get { - return slice.count - } - set(newValue) { - ensureUniqueReference() - let difference = newValue - count - if difference > 0 { - let additionalRange = Int(slice.upperBound) ..< Int(slice.upperBound) + difference - storage.resetBytes(in: additionalRange) // Already sets the length - } else { - storage.length += difference - } - slice.range = slice.range.lowerBound..<(slice.range.lowerBound + newValue) - } - } - - @inlinable // This is @inlinable as it is trivially forwarding. - var range: Range { - return slice.range - } - - @inlinable // This is @inlinable as a generic, trivially forwarding function. - func withUnsafeBytes(_ apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { - return try storage.withUnsafeBytes(in: range, apply: apply) - } - - @inlinable // This is @inlinable as a generic, trivially forwarding function. - mutating func withUnsafeMutableBytes(_ apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { - ensureUniqueReference() - return try storage.withUnsafeMutableBytes(in: range, apply: apply) - } - - @inlinable // This is @inlinable as reasonably small. - mutating func append(contentsOf buffer: UnsafeRawBufferPointer) { - ensureUniqueReference() - let upperbound = storage.length + storage._offset - #if FOUNDATION_FRAMEWORK - if #available(macOS 14, iOS 17, watchOS 10, tvOS 17, *) { - storage.replaceBytes( - in: range.upperBound ..< upperbound, - with: buffer.baseAddress, - length: buffer.count) - } else { - storage.replaceBytes( - in: NSRange( - location: range.upperBound, - length: storage.length - (range.upperBound - storage._offset)), - with: buffer.baseAddress, - length: buffer.count) - } - #else - storage.replaceBytes(in: range.upperBound ..< upperbound, with: buffer.baseAddress, length: buffer.count) - #endif - slice.range = slice.range.lowerBound.. UInt8 { - get { - precondition(startIndex <= index, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(index < endIndex, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") - return storage.get(index) - } - set(newValue) { - precondition(startIndex <= index, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(index < endIndex, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") - ensureUniqueReference() - storage.set(index, to: newValue) - } - } - - @inlinable // This is @inlinable as reasonably small. - mutating func resetBytes(in range: Range) { - precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - ensureUniqueReference() - storage.resetBytes(in: range) - if slice.range.upperBound < range.upperBound { - slice.range = slice.range.lowerBound.., with bytes: UnsafeRawPointer?, count cnt: Int) { - precondition(startIndex <= subrange.lowerBound, "index \(subrange.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(subrange.lowerBound <= endIndex, "index \(subrange.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(startIndex <= subrange.upperBound, "index \(subrange.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(subrange.upperBound <= endIndex, "index \(subrange.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - - ensureUniqueReference() - let upper = range.upperBound - #if FOUNDATION_FRAMEWORK - if #available(macOS 14, iOS 17, watchOS 10, tvOS 17, *) { - storage.replaceBytes(in: subrange, with: bytes, length: cnt) - } else { - let nsRange = NSRange( - location: subrange.lowerBound, - length: subrange.upperBound - subrange.lowerBound) - storage.replaceBytes(in: nsRange, with: bytes, length: cnt) - } - #else - storage.replaceBytes(in: subrange, with: bytes, length: cnt) - #endif - let resultingUpper = upper - (subrange.upperBound - subrange.lowerBound) + cnt - slice.range = slice.range.lowerBound..) { - precondition(startIndex <= range.lowerBound, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(startIndex <= range.upperBound, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - precondition(range.upperBound <= endIndex, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") - storage.copyBytes(to: pointer, from: range) - } - - @inline(__always) // This should always be inlined into _Representation.hash(into:). - func hash(into hasher: inout Hasher) { - hasher.combine(count) - - // Hash at most the first 80 bytes of this data. - let range = startIndex ..< Swift.min(startIndex + 80, endIndex) - storage.withUnsafeBytes(in: range) { - hasher.combine(bytes: $0) - } - } - } - - // The actual storage for Data's various representations. - // Inlinability strategy: almost everything should be inlinable as forwarding the underlying implementations. (Inlining can also help avoid retain-release traffic around pulling values out of enums.) - @usableFromInline - @frozen - internal enum _Representation : Sendable { - case empty - case inline(InlineData) - case slice(InlineSlice) - case large(LargeSlice) - - @inlinable // This is @inlinable as a trivial initializer. - init(_ buffer: UnsafeRawBufferPointer) { - if buffer.isEmpty { - self = .empty - } else if InlineData.canStore(count: buffer.count) { - self = .inline(InlineData(buffer)) - } else if InlineSlice.canStore(count: buffer.count) { - self = .slice(InlineSlice(buffer)) - } else { - self = .large(LargeSlice(buffer)) - } - } - - @inlinable // This is @inlinable as a trivial initializer. - init(_ buffer: UnsafeRawBufferPointer, owner: AnyObject) { - if buffer.isEmpty { - self = .empty - } else if InlineData.canStore(count: buffer.count) { - self = .inline(InlineData(buffer)) - } else { - let count = buffer.count - let storage = __DataStorage(bytes: UnsafeMutableRawPointer(mutating: buffer.baseAddress), length: count, copy: false, deallocator: { _, _ in - _fixLifetime(owner) - }, offset: 0) - if InlineSlice.canStore(count: count) { - self = .slice(InlineSlice(storage, count: count)) - } else { - self = .large(LargeSlice(storage, count: count)) - } - } - } - - @inlinable // This is @inlinable as a trivial initializer. - init(capacity: Int) { - if capacity == 0 { - self = .empty - } else if InlineData.canStore(count: capacity) { - self = .inline(InlineData()) - } else if InlineSlice.canStore(count: capacity) { - self = .slice(InlineSlice(capacity: capacity)) - } else { - self = .large(LargeSlice(capacity: capacity)) - } - } - - @inlinable // This is @inlinable as a trivial initializer. - init(count: Int) { - if count == 0 { - self = .empty - } else if InlineData.canStore(count: count) { - self = .inline(InlineData(count: count)) - } else if InlineSlice.canStore(count: count) { - self = .slice(InlineSlice(count: count)) - } else { - self = .large(LargeSlice(count: count)) - } - } - - @inlinable // This is @inlinable as a trivial initializer. - init(_ storage: __DataStorage, count: Int) { - if count == 0 { - self = .empty - } else if InlineData.canStore(count: count) { - self = .inline(storage.withUnsafeBytes(in: 0.. 0 else { return } - switch self { - case .empty: - if InlineData.canStore(count: minimumCapacity) { - self = .inline(InlineData()) - } else if InlineSlice.canStore(count: minimumCapacity) { - self = .slice(InlineSlice(capacity: minimumCapacity)) - } else { - self = .large(LargeSlice(capacity: minimumCapacity)) - } - case .inline(let inline): - guard minimumCapacity > inline.capacity else { return } - // we know we are going to be heap promoted - if InlineSlice.canStore(count: minimumCapacity) { - var slice = InlineSlice(inline) - slice.reserveCapacity(minimumCapacity) - self = .slice(slice) - } else { - var slice = LargeSlice(inline) - slice.reserveCapacity(minimumCapacity) - self = .large(slice) - } - case .slice(var slice): - guard minimumCapacity > slice.capacity else { return } - if InlineSlice.canStore(count: minimumCapacity) { - self = .empty - slice.reserveCapacity(minimumCapacity) - self = .slice(slice) - } else { - var large = LargeSlice(slice) - large.reserveCapacity(minimumCapacity) - self = .large(large) - } - case .large(var slice): - guard minimumCapacity > slice.capacity else { return } - self = .empty - slice.reserveCapacity(minimumCapacity) - self = .large(slice) - } - } - - @inlinable // This is @inlinable as reasonably small. - var count: Int { - get { - switch self { - case .empty: return 0 - case .inline(let inline): return inline.count - case .slice(let slice): return slice.count - case .large(let slice): return slice.count - } - } - set(newValue) { - // HACK: The definition of this inline function takes an inout reference to self, giving the optimizer a unique referencing guarantee. - // This allows us to avoid excessive retain-release traffic around modifying enum values, and inlining the function then avoids the additional frame. - @inline(__always) - func apply(_ representation: inout _Representation, _ newValue: Int) -> _Representation? { - switch representation { - case .empty: - if newValue == 0 { - return nil - } else if InlineData.canStore(count: newValue) { - return .inline(InlineData(count: newValue)) - } else if InlineSlice.canStore(count: newValue) { - return .slice(InlineSlice(count: newValue)) - } else { - return .large(LargeSlice(count: newValue)) - } - case .inline(var inline): - if newValue == 0 { - return .empty - } else if InlineData.canStore(count: newValue) { - guard inline.count != newValue else { return nil } - inline.count = newValue - return .inline(inline) - } else if InlineSlice.canStore(count: newValue) { - var slice = InlineSlice(inline) - slice.count = newValue - return .slice(slice) - } else { - var slice = LargeSlice(inline) - slice.count = newValue - return .large(slice) - } - case .slice(var slice): - if newValue == 0 && slice.startIndex == 0 { - return .empty - } else if slice.startIndex == 0 && InlineData.canStore(count: newValue) { - return .inline(InlineData(slice, count: newValue)) - } else if InlineSlice.canStore(count: newValue + slice.startIndex) { - guard slice.count != newValue else { return nil } - representation = .empty // TODO: remove this when mgottesman lands optimizations - slice.count = newValue - return .slice(slice) - } else { - var newSlice = LargeSlice(slice) - newSlice.count = newValue - return .large(newSlice) - } - case .large(var slice): - if newValue == 0 && slice.startIndex == 0 { - return .empty - } else if slice.startIndex == 0 && InlineData.canStore(count: newValue) { - return .inline(InlineData(slice, count: newValue)) - } else { - guard slice.count != newValue else { return nil} - representation = .empty // TODO: remove this when mgottesman lands optimizations - slice.count = newValue - return .large(slice) - } - } - } - - if let rep = apply(&self, newValue) { - self = rep - } - } - } - - @inlinable // This is @inlinable as a generic, trivially forwarding function. - func withUnsafeBytes(_ apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { - switch self { - case .empty: - let empty = InlineData() - return try empty.withUnsafeBytes(apply) - case .inline(let inline): - return try inline.withUnsafeBytes(apply) - case .slice(let slice): - return try slice.withUnsafeBytes(apply) - case .large(let slice): - return try slice.withUnsafeBytes(apply) - } - } - - @inlinable // This is @inlinable as a generic, trivially forwarding function. - mutating func withUnsafeMutableBytes(_ apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { - switch self { - case .empty: - var empty = InlineData() - return try empty.withUnsafeMutableBytes(apply) - case .inline(var inline): - defer { self = .inline(inline) } - return try inline.withUnsafeMutableBytes(apply) - case .slice(var slice): - self = .empty - defer { self = .slice(slice) } - return try slice.withUnsafeMutableBytes(apply) - case .large(var slice): - self = .empty - defer { self = .large(slice) } - return try slice.withUnsafeMutableBytes(apply) - } - } - - @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. - func enumerateBytes(_ block: (_ buffer: UnsafeBufferPointer, _ byteIndex: Index, _ stop: inout Bool) -> Void) { - switch self { - case .empty: - var stop = false - block(UnsafeBufferPointer(start: nil, count: 0), 0, &stop) - case .inline(let inline): - inline.withUnsafeBytes { - var stop = false - $0.withMemoryRebound(to: UInt8.self) { block($0, 0, &stop) } - } - case .slice(let slice): - slice.storage.enumerateBytes(in: slice.range, block) - case .large(let slice): - slice.storage.enumerateBytes(in: slice.range, block) - } - } - - @inlinable // This is @inlinable as reasonably small. - mutating func append(contentsOf buffer: UnsafeRawBufferPointer) { - switch self { - case .empty: - self = _Representation(buffer) - case .inline(var inline): - if InlineData.canStore(count: inline.count + buffer.count) { - inline.append(contentsOf: buffer) - self = .inline(inline) - } else if InlineSlice.canStore(count: inline.count + buffer.count) { - var newSlice = InlineSlice(inline) - newSlice.append(contentsOf: buffer) - self = .slice(newSlice) - } else { - var newSlice = LargeSlice(inline) - newSlice.append(contentsOf: buffer) - self = .large(newSlice) - } - case .slice(var slice): - if InlineSlice.canStore(count: slice.range.upperBound + buffer.count) { - self = .empty - defer { self = .slice(slice) } - slice.append(contentsOf: buffer) - } else { - self = .empty - var newSlice = LargeSlice(slice) - newSlice.append(contentsOf: buffer) - self = .large(newSlice) - } - case .large(var slice): - self = .empty - defer { self = .large(slice) } - slice.append(contentsOf: buffer) - } - } - - @inlinable // This is @inlinable as reasonably small. - mutating func resetBytes(in range: Range) { - switch self { - case .empty: - if range.upperBound == 0 { - self = .empty - } else if InlineData.canStore(count: range.upperBound) { - precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - self = .inline(InlineData(count: range.upperBound)) - } else if InlineSlice.canStore(count: range.upperBound) { - precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - self = .slice(InlineSlice(count: range.upperBound)) - } else { - precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") - self = .large(LargeSlice(count: range.upperBound)) - } - case .inline(var inline): - if inline.count < range.upperBound { - if InlineSlice.canStore(count: range.upperBound) { - var slice = InlineSlice(inline) - slice.resetBytes(in: range) - self = .slice(slice) - } else { - var slice = LargeSlice(inline) - slice.resetBytes(in: range) - self = .large(slice) - } - } else { - inline.resetBytes(in: range) - self = .inline(inline) - } - case .slice(var slice): - if InlineSlice.canStore(count: range.upperBound) { - self = .empty - slice.resetBytes(in: range) - self = .slice(slice) - } else { - self = .empty - var newSlice = LargeSlice(slice) - newSlice.resetBytes(in: range) - self = .large(newSlice) - } - case .large(var slice): - self = .empty - slice.resetBytes(in: range) - self = .large(slice) - } - } - - @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. - mutating func replaceSubrange(_ subrange: Range, with bytes: UnsafeRawPointer?, count cnt: Int) { - switch self { - case .empty: - precondition(subrange.lowerBound == 0 && subrange.upperBound == 0, "range \(subrange) out of bounds of 0..<0") - if cnt == 0 { - return - } else if InlineData.canStore(count: cnt) { - self = .inline(InlineData(UnsafeRawBufferPointer(start: bytes, count: cnt))) - } else if InlineSlice.canStore(count: cnt) { - self = .slice(InlineSlice(UnsafeRawBufferPointer(start: bytes, count: cnt))) - } else { - self = .large(LargeSlice(UnsafeRawBufferPointer(start: bytes, count: cnt))) - } - case .inline(var inline): - let resultingCount = inline.count + cnt - (subrange.upperBound - subrange.lowerBound) - if resultingCount == 0 { - self = .empty - } else if InlineData.canStore(count: resultingCount) { - inline.replaceSubrange(subrange, with: bytes, count: cnt) - self = .inline(inline) - } else if InlineSlice.canStore(count: resultingCount) { - var slice = InlineSlice(inline) - slice.replaceSubrange(subrange, with: bytes, count: cnt) - self = .slice(slice) - } else { - var slice = LargeSlice(inline) - slice.replaceSubrange(subrange, with: bytes, count: cnt) - self = .large(slice) - } - case .slice(var slice): - let resultingUpper = slice.endIndex + cnt - (subrange.upperBound - subrange.lowerBound) - if slice.startIndex == 0 && resultingUpper == 0 { - self = .empty - } else if slice.startIndex == 0 && InlineData.canStore(count: resultingUpper) { - self = .empty - slice.replaceSubrange(subrange, with: bytes, count: cnt) - self = .inline(InlineData(slice, count: slice.count)) - } else if InlineSlice.canStore(count: resultingUpper) { - self = .empty - slice.replaceSubrange(subrange, with: bytes, count: cnt) - self = .slice(slice) - } else { - self = .empty - var newSlice = LargeSlice(slice) - newSlice.replaceSubrange(subrange, with: bytes, count: cnt) - self = .large(newSlice) - } - case .large(var slice): - let resultingUpper = slice.endIndex + cnt - (subrange.upperBound - subrange.lowerBound) - if slice.startIndex == 0 && resultingUpper == 0 { - self = .empty - } else if slice.startIndex == 0 && InlineData.canStore(count: resultingUpper) { - var inline = InlineData(count: resultingUpper) - inline.withUnsafeMutableBytes { inlineBuffer in - if cnt > 0 { - inlineBuffer.baseAddress?.advanced(by: subrange.lowerBound).copyMemory(from: bytes!, byteCount: cnt) - } - slice.withUnsafeBytes { buffer in - if subrange.lowerBound > 0 { - inlineBuffer.baseAddress?.copyMemory(from: buffer.baseAddress!, byteCount: subrange.lowerBound) - } - if subrange.upperBound < resultingUpper { - inlineBuffer.baseAddress?.advanced(by: subrange.upperBound).copyMemory(from: buffer.baseAddress!.advanced(by: subrange.upperBound), byteCount: resultingUpper - subrange.upperBound) - } - } - } - self = .inline(inline) - } else if InlineSlice.canStore(count: slice.startIndex) && InlineSlice.canStore(count: resultingUpper) { - self = .empty - var newSlice = InlineSlice(slice) - newSlice.replaceSubrange(subrange, with: bytes, count: cnt) - self = .slice(newSlice) - } else { - self = .empty - slice.replaceSubrange(subrange, with: bytes, count: cnt) - self = .large(slice) - } - } - } - - @inlinable // This is @inlinable as trivially forwarding. - subscript(index: Index) -> UInt8 { - get { - switch self { - case .empty: preconditionFailure("index \(index) out of range of 0") - case .inline(let inline): return inline[index] - case .slice(let slice): return slice[index] - case .large(let slice): return slice[index] - } - } - set(newValue) { - switch self { - case .empty: preconditionFailure("index \(index) out of range of 0") - case .inline(var inline): - inline[index] = newValue - self = .inline(inline) - case .slice(var slice): - self = .empty - slice[index] = newValue - self = .slice(slice) - case .large(var slice): - self = .empty - slice[index] = newValue - self = .large(slice) - } - } - } - - @inlinable // This is @inlinable as reasonably small. - subscript(bounds: Range) -> Data { - get { - switch self { - case .empty: - precondition(bounds.lowerBound == 0 && (bounds.upperBound - bounds.lowerBound) == 0, "Range \(bounds) out of bounds 0..<0") - return Data() - case .inline(let inline): - precondition(bounds.upperBound <= inline.count, "Range \(bounds) out of bounds 0..<\(inline.count)") - if bounds.lowerBound == 0 { - var newInline = inline - newInline.count = bounds.upperBound - return Data(representation: .inline(newInline)) - } else { - return Data(representation: .slice(InlineSlice(inline, range: bounds))) - } - case .slice(let slice): - precondition(slice.startIndex <= bounds.lowerBound, "Range \(bounds) out of bounds \(slice.range)") - precondition(bounds.lowerBound <= slice.endIndex, "Range \(bounds) out of bounds \(slice.range)") - precondition(slice.startIndex <= bounds.upperBound, "Range \(bounds) out of bounds \(slice.range)") - precondition(bounds.upperBound <= slice.endIndex, "Range \(bounds) out of bounds \(slice.range)") - if bounds.lowerBound == 0 && bounds.upperBound == 0 { - return Data() - } else if bounds.lowerBound == 0 && InlineData.canStore(count: bounds.count) { - return Data(representation: .inline(InlineData(slice, count: bounds.count))) - } else { - var newSlice = slice - newSlice.range = bounds - return Data(representation: .slice(newSlice)) - } - case .large(let slice): - precondition(slice.startIndex <= bounds.lowerBound, "Range \(bounds) out of bounds \(slice.range)") - precondition(bounds.lowerBound <= slice.endIndex, "Range \(bounds) out of bounds \(slice.range)") - precondition(slice.startIndex <= bounds.upperBound, "Range \(bounds) out of bounds \(slice.range)") - precondition(bounds.upperBound <= slice.endIndex, "Range \(bounds) out of bounds \(slice.range)") - if bounds.lowerBound == 0 && bounds.upperBound == 0 { - return Data() - } else if bounds.lowerBound == 0 && InlineData.canStore(count: bounds.upperBound) { - return Data(representation: .inline(InlineData(slice, count: bounds.upperBound))) - } else if InlineSlice.canStore(count: bounds.lowerBound) && InlineSlice.canStore(count: bounds.upperBound) { - return Data(representation: .slice(InlineSlice(slice, range: bounds))) - } else { - var newSlice = slice - newSlice.slice = RangeReference(bounds) - return Data(representation: .large(newSlice)) - } - } - } - } - - @inlinable // This is @inlinable as trivially forwarding. - var startIndex: Int { - switch self { - case .empty: return 0 - case .inline: return 0 - case .slice(let slice): return slice.startIndex - case .large(let slice): return slice.startIndex - } - } - - @inlinable // This is @inlinable as trivially forwarding. - var endIndex: Int { - switch self { - case .empty: return 0 - case .inline(let inline): return inline.count - case .slice(let slice): return slice.endIndex - case .large(let slice): return slice.endIndex - } + // Use an inline allocation for 32 bytes or fewer + if capacity <= 32 { + withUnsafeTemporaryAllocation(of: UInt8.self, capacity: capacity) { buffer in + body(buffer) } + return + } - @inlinable // This is @inlinable as trivially forwarding. - func copyBytes(to pointer: UnsafeMutableRawPointer, from range: Range) { - switch self { - case .empty: - precondition(range.lowerBound == 0 && range.upperBound == 0, "Range \(range) out of bounds 0..<0") - return - case .inline(let inline): - inline.copyBytes(to: pointer, from: range) - case .slice(let slice): - slice.copyBytes(to: pointer, from: range) - case .large(let slice): - slice.copyBytes(to: pointer, from: range) - } - } + let buffer = UnsafeMutableBufferPointer.allocate(capacity: capacity) + defer { buffer.deallocate() } + body(buffer) +} - @inline(__always) // This should always be inlined into Data.hash(into:). - func hash(into hasher: inout Hasher) { - switch self { - case .empty: - hasher.combine(0) - case .inline(let inline): - inline.hash(into: &hasher) - case .slice(let slice): - slice.hash(into: &hasher) - case .large(let large): - large.hash(into: &hasher) - } - } - } +@frozen +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +#if compiler(>=6.2) +@_addressableForDependencies +#endif +public struct Data : RandomAccessCollection, MutableCollection, RangeReplaceableCollection, Sendable, Hashable { + public typealias Index = Int + public typealias Indices = Range @usableFromInline internal var _representation: _Representation @@ -2069,101 +316,10 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect } } - @available(swift, introduced: 4.2) - @available(swift, deprecated: 5, message: "use `init(_:)` instead") - public init(bytes elements: S) where S.Iterator.Element == UInt8 { - self.init(elements) - } - - @available(swift, obsoleted: 4.2) - public init(bytes: Array) { - self.init(bytes) - } - - @available(swift, obsoleted: 4.2) - public init(bytes: ArraySlice) { - self.init(bytes) - } - @inlinable // This is @inlinable as a trivial initializer. internal init(representation: _Representation) { _representation = representation } - -#if FOUNDATION_FRAMEWORK - public typealias ReadingOptions = NSData.ReadingOptions - public typealias WritingOptions = NSData.WritingOptions -#else - public struct ReadingOptions : OptionSet, Sendable { - public let rawValue: UInt - public init(rawValue: UInt) { self.rawValue = rawValue } - - public static let mappedIfSafe = ReadingOptions(rawValue: 1 << 0) - public static let uncached = ReadingOptions(rawValue: 1 << 1) - public static let alwaysMapped = ReadingOptions(rawValue: 1 << 3) - } - - // This is imported from the ObjC 'option set', which is actually a combination of an option and an enumeration (file protection). - public struct WritingOptions : OptionSet, Sendable { - public let rawValue: UInt - public init(rawValue: UInt) { self.rawValue = rawValue } - - /// An option to write data to an auxiliary file first and then replace the original file with the auxiliary file when the write completes. -#if os(WASI) - @available(*, unavailable, message: "atomic writing is unavailable in WASI because temporary files are not supported") -#endif - public static let atomic = WritingOptions(rawValue: 1 << 0) - - /// An option that attempts to write data to a file and fails with an error if the destination file already exists. - public static let withoutOverwriting = WritingOptions(rawValue: 1 << 1) - - /// An option to not encrypt the file when writing it out. - public static let noFileProtection = WritingOptions(rawValue: 0x10000000) - - /// An option to make the file accessible only while the device is unlocked. - public static let completeFileProtection = WritingOptions(rawValue: 0x20000000) - - /// An option to allow the file to be accessible while the device is unlocked or the file is already open. - public static let completeFileProtectionUnlessOpen = WritingOptions(rawValue: 0x30000000) - - /// An option to allow the file to be accessible after a user first unlocks the device. - public static let completeFileProtectionUntilFirstUserAuthentication = WritingOptions(rawValue: 0x40000000) - - /// An option the system uses when determining the file protection options that the system assigns to the data. - public static let fileProtectionMask = WritingOptions(rawValue: 0xf0000000) - } -#endif - - #if !FOUNDATION_FRAMEWORK - @_spi(SwiftCorelibsFoundation) - public dynamic init(_contentsOfRemote url: URL, options: ReadingOptions = []) throws { - assert(!url.isFileURL) - throw CocoaError(.fileReadUnsupportedScheme) - } - #endif - - /// Initialize a `Data` with the contents of a `URL`. - /// - /// - parameter url: The `URL` to read. - /// - parameter options: Options for the read operation. Default value is `[]`. - /// - throws: An error in the Cocoa domain, if `url` cannot be read. - public init(contentsOf url: __shared URL, options: ReadingOptions = []) throws { - if url.isFileURL { - self = try readDataFromFile(path: .url(url), reportProgress: true, options: options) - } else { - #if FOUNDATION_FRAMEWORK - // Fallback to NSData, to read via NSURLSession - let d = try NSData(contentsOf: url, options: NSData.ReadingOptions(rawValue: options.rawValue)) - self.init(referencing: d) - #else - try self.init(_contentsOfRemote: url, options: options) - #endif - } - } - - internal init(contentsOfFile path: String, options: ReadingOptions = []) throws { - self = try readDataFromFile(path: .path(path), reportProgress: true, options: options) - } // ----------------------------------- // MARK: - Properties and Functions @@ -2189,21 +345,6 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect } } - @inlinable // This is @inlinable as trivially computable. - public var regions: CollectionOfOne { - return CollectionOfOne(self) - } - - /// Access the bytes in the data. - /// - /// - warning: The byte pointer argument should not be stored and used outside of the lifetime of the call to the closure. - @available(swift, deprecated: 5, message: "use `withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` instead") - public func withUnsafeBytes(_ body: (UnsafePointer) throws -> ResultType) rethrows -> ResultType { - return try _representation.withUnsafeBytes { - return try body($0.baseAddress?.assumingMemoryBound(to: ContentType.self) ?? UnsafePointer(bitPattern: 0xBAD0)!) - } - } - @inlinable // This is @inlinable as a generic, trivially forwarding function. public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> ResultType) rethrows -> ResultType { return try _representation.withUnsafeBytes(body) @@ -2218,10 +359,10 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect switch _representation { case .empty: buffer = UnsafeRawBufferPointer(start: nil, count: 0) - case .inline: + case .inline(let inline): buffer = unsafe UnsafeRawBufferPointer( start: UnsafeRawPointer(Builtin.addressOfBorrow(self)), - count: _representation.count + count: inline.count ) case .large(let slice): buffer = unsafe UnsafeRawBufferPointer( @@ -2256,10 +397,10 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect switch _representation { case .empty: buffer = UnsafeMutableRawBufferPointer(start: nil, count: 0) - case .inline: + case .inline(let inline): buffer = unsafe UnsafeMutableRawBufferPointer( start: UnsafeMutableRawPointer(Builtin.addressOfBorrow(self)), - count: _representation.count + count: inline.count ) case .large(var slice): // Clear _representation during the unique check to avoid double counting the reference, and assign the mutated slice back to _representation afterwards @@ -2297,10 +438,10 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect switch _representation { case .empty: buffer = UnsafeMutableRawBufferPointer(start: nil, count: 0) - case .inline: + case .inline(let inline): buffer = unsafe UnsafeMutableRawBufferPointer( start: UnsafeMutableRawPointer(Builtin.addressOfBorrow(self)), - count: _representation.count + count: inline.count ) case .large(var slice): // Clear _representation during the unique check to avoid double counting the reference, and assign the mutated slice back to _representation afterwards @@ -2332,89 +473,12 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect } } - /// Mutate the bytes in the data. - /// - /// This function assumes that you are mutating the contents. - /// - warning: The byte pointer argument should not be stored and used outside of the lifetime of the call to the closure. - @available(swift, deprecated: 5, message: "use `withUnsafeMutableBytes(_: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R` instead") - public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutablePointer) throws -> ResultType) rethrows -> ResultType { - return try _representation.withUnsafeMutableBytes { - return try body($0.baseAddress?.assumingMemoryBound(to: ContentType.self) ?? UnsafeMutablePointer(bitPattern: 0xBAD0)!) - } - } - @inlinable // This is @inlinable as a generic, trivially forwarding function. public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> ResultType) rethrows -> ResultType { return try _representation.withUnsafeMutableBytes(body) } // MARK: - - // MARK: Copy Bytes - - /// Copy the contents of the data to a pointer. - /// - /// - parameter pointer: A pointer to the buffer you wish to copy the bytes into. - /// - parameter count: The number of bytes to copy. - /// - warning: This method does not verify that the contents at pointer have enough space to hold `count` bytes. - @inlinable // This is @inlinable as trivially forwarding. - public func copyBytes(to pointer: UnsafeMutablePointer, count: Int) { - precondition(count >= 0, "count of bytes to copy must not be negative") - if count == 0 { return } - _copyBytesHelper(to: UnsafeMutableRawPointer(pointer), from: startIndex..<(startIndex + count)) - } - - @inlinable // This is @inlinable as trivially forwarding. - internal func _copyBytesHelper(to pointer: UnsafeMutableRawPointer, from range: Range) { - if range.isEmpty { return } - _representation.copyBytes(to: pointer, from: range) - } - - /// Copy a subset of the contents of the data to a pointer. - /// - /// - parameter pointer: A pointer to the buffer you wish to copy the bytes into. - /// - parameter range: The range in the `Data` to copy. - /// - warning: This method does not verify that the contents at pointer have enough space to hold the required number of bytes. - @inlinable // This is @inlinable as trivially forwarding. - public func copyBytes(to pointer: UnsafeMutablePointer, from range: Range) { - _copyBytesHelper(to: pointer, from: range) - } - - // Copy the contents of the data into a buffer. - /// - /// This function copies the bytes in `range` from the data into the buffer. If the count of the `range` is greater than `MemoryLayout.stride * buffer.count` then the first N bytes will be copied into the buffer. - /// - precondition: The range must be within the bounds of the data. Otherwise `fatalError` is called. - /// - parameter buffer: A buffer to copy the data into. - /// - parameter range: A range in the data to copy into the buffer. If the range is empty, this function will return 0 without copying anything. If the range is nil, as much data as will fit into `buffer` is copied. - /// - returns: Number of bytes copied into the destination buffer. - @inlinable // This is @inlinable as generic and reasonably small. - public func copyBytes(to buffer: UnsafeMutableBufferPointer, from range: Range? = nil) -> Int { - let cnt = count - guard cnt > 0 else { return 0 } - - let copyRange : Range - if let r = range { - guard !r.isEmpty else { return 0 } - copyRange = r.lowerBound..<(r.lowerBound + Swift.min(buffer.count * MemoryLayout.stride, r.upperBound - r.lowerBound)) - } else { - copyRange = startIndex..<(startIndex + Swift.min(buffer.count * MemoryLayout.stride, cnt)) - } - - guard !copyRange.isEmpty else { return 0 } - - _copyBytesHelper(to: buffer.baseAddress!, from: copyRange) - return copyRange.upperBound - copyRange.lowerBound - } - - // MARK: - - - /// Enumerate the contents of the data. - /// - /// In some cases, (for example, a `Data` backed by a `dispatch_data_t`, the bytes may be stored discontinuously. In those cases, this function invokes the closure for each contiguous region of bytes. - /// - parameter block: The closure to invoke for each region of data. You may stop the enumeration by setting the `stop` parameter to `true`. - @available(swift, deprecated: 5, message: "use `regions` or `for-in` instead") - public func enumerateBytes(_ block: (_ buffer: UnsafeBufferPointer, _ byteIndex: Index, _ stop: inout Bool) -> Void) { - _representation.enumerateBytes(block) - } @inlinable // This is @inlinable as a generic, trivially forwarding function. internal mutating func _append(_ buffer : UnsafeBufferPointer) { @@ -2595,49 +659,16 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect } } - // MARK: - - - /// Write the contents of the `Data` to a location. - /// - /// - parameter url: The location to write the data into. - /// - parameter options: Options for writing the data. Default value is `[]`. - /// - throws: An error in the Cocoa domain, if there is an error writing to the `URL`. - public func write(to url: URL, options: Data.WritingOptions = []) throws { -#if !os(WASI) // `.atomic` is unavailable on WASI - if options.contains(.withoutOverwriting) && options.contains(.atomic) { - fatalError("withoutOverwriting is not supported with atomic") - } -#endif - - guard url.isFileURL else { - throw CocoaError(.fileWriteUnsupportedScheme) - } - -#if !NO_FILESYSTEM - try writeToFile(path: .url(url), data: self, options: options, reportProgress: true) -#else - throw CocoaError(.featureUnsupported) -#endif - } - // MARK: - // - /// The hash value for the data. - @inline(never) // This is not inlinable as emission into clients could cause cross-module inconsistencies if they are not all recompiled together. - public func hash(into hasher: inout Hasher) { - _representation.hash(into: &hasher) - } - public func advanced(by amount: Int) -> Data { precondition(amount >= 0) let start = self.index(self.startIndex, offsetBy: amount) precondition(start <= self.endIndex) return Data(self[start...]) } - - // MARK: - - + // MARK: - // MARK: Index and Subscript @@ -2732,106 +763,26 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect return (Iterator(self, at: startIndex + cnt), buffer.index(buffer.startIndex, offsetBy: cnt)) } +} - /// An iterator over the contents of the data. - /// - /// The iterator will increment byte-by-byte. - @inlinable // This is @inlinable as trivially computable. - public func makeIterator() -> Data.Iterator { - return Iterator(self, at: startIndex) - } - - public struct Iterator : IteratorProtocol, Sendable { - @usableFromInline - internal typealias Buffer = ( - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, - UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) - - @usableFromInline internal let _data: Data - @usableFromInline internal var _buffer: Buffer - @usableFromInline internal var _idx: Data.Index - @usableFromInline internal let _endIdx: Data.Index - - @usableFromInline // This is @usableFromInline as a non-trivial initializer. - internal init(_ data: Data, at loc: Data.Index) { - // The let vars prevent this from being marked as @inlinable - _data = data - _buffer = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) - _idx = loc - _endIdx = data.endIndex - - let bufferSize = MemoryLayout.size - Swift.withUnsafeMutableBytes(of: &_buffer) { - $0.withMemoryRebound(to: UInt8.self) { [endIndex = data.endIndex] buf in - let bufferIdx = (loc - data.startIndex) % bufferSize - let end = (endIndex - (loc - bufferIdx) > bufferSize) ? (loc - bufferIdx + bufferSize) : endIndex - data.copyBytes(to: buf, from: (loc - bufferIdx).. UInt8? { - let idx = _idx - let bufferSize = MemoryLayout.size - - guard idx < _endIdx else { return nil } - _idx += 1 - - let bufferIdx = (idx - _data.startIndex) % bufferSize - - - if bufferIdx == 0 { - var buffer = _buffer - Swift.withUnsafeMutableBytes(of: &buffer) { - $0.withMemoryRebound(to: UInt8.self) { - // populate the buffer - _data.copyBytes(to: $0, from: idx..<(_endIdx - idx > bufferSize ? idx + bufferSize : _endIdx)) - } - } - _buffer = buffer - } - - return Swift.withUnsafeMutableBytes(of: &_buffer) { - $0.load(fromByteOffset: bufferIdx, as: UInt8.self) - } - } - } +@available(macOS, unavailable, introduced: 10.10) +@available(iOS, unavailable, introduced: 8.0) +@available(tvOS, unavailable, introduced: 9.0) +@available(watchOS, unavailable, introduced: 2.0) +@available(*, unavailable) +extension Data.Deallocator : Sendable {} - // MARK: - Range - -#if FOUNDATION_FRAMEWORK - /// Find the given `Data` in the content of this `Data`. - /// - /// - parameter dataToFind: The data to be searched for. - /// - parameter options: Options for the search. Default value is `[]`. - /// - parameter range: The range of this data in which to perform the search. Default value is `nil`, which means the entire content of this data. - /// - returns: A `Range` specifying the location of the found data, or nil if a match could not be found. - /// - precondition: `range` must be in the bounds of the Data. - public func range(of dataToFind: Data, options: Data.SearchOptions = [], in range: Range? = nil) -> Range? { - let nsRange : NSRange - if let r = range { - nsRange = NSRange(location: r.lowerBound - startIndex, length: r.upperBound - r.lowerBound) - } else { - nsRange = NSRange(location: 0, length: count) - } - let result = _representation.withInteriorPointerReference { - let opts = NSData.SearchOptions(rawValue: options.rawValue) - return $0.range(of: dataToFind, options: opts, in: nsRange) - } - if result.location == NSNotFound { - return nil - } - return (result.location + startIndex)..<((result.location + startIndex) + result.length) +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { + /// The hash value for the data. + @inline(never) // This is not inlinable as emission into clients could cause cross-module inconsistencies if they are not all recompiled together. + public func hash(into hasher: inout Hasher) { + _representation.hash(into: &hasher) } -#else - // TODO: Implement range(of:options:in:) for Foundation package. -#endif - - // MARK: - - // +} +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { /// Returns `true` if the two `Data` arguments are equal. @inlinable // This is @inlinable as emission into clients is safe -- the concept of equality on Data will not change. public static func ==(d1 : Data, d2 : Data) -> Bool { @@ -2864,7 +815,8 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect } // Compare the contents - return memcmp(b1Address, b2Address, b2.count) == 0 + assert(length1 == b2.count) + return memcmp(b1Address, b2Address, length1) == 0 } } } @@ -2872,76 +824,6 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect } } -@available(macOS, unavailable, introduced: 10.10) -@available(iOS, unavailable, introduced: 8.0) -@available(tvOS, unavailable, introduced: 9.0) -@available(watchOS, unavailable, introduced: 2.0) -@available(*, unavailable) -extension Data.Deallocator : Sendable {} - -#if !FOUNDATION_FRAMEWORK -// MARK: Exported Types -extension Data { - public struct SearchOptions : OptionSet, Sendable { - public let rawValue: UInt - - public init(rawValue: UInt) { - self.rawValue = rawValue - } - /// Search from the end of the data object. - public static let backwards = SearchOptions(rawValue: 1 << 0) - /// Search is limited to start (or end, if searching backwards) of the data object. - public static let anchored = SearchOptions(rawValue: 1 << 1) - } - - @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) - public struct Base64EncodingOptions : OptionSet, Sendable { - public let rawValue: UInt - - public init(rawValue: UInt) { - self.rawValue = rawValue - } - /// Set the maximum line length to 64 characters, after which a line ending is inserted. - public static let lineLength64Characters = Base64EncodingOptions(rawValue: 1 << 0) - /// Set the maximum line length to 76 characters, after which a line ending is inserted. - public static let lineLength76Characters = Base64EncodingOptions(rawValue: 1 << 1) - /// When a maximum line length is set, specify that the line ending to insert should include a carriage return. - public static let endLineWithCarriageReturn = Base64EncodingOptions(rawValue: 1 << 4) - /// When a maximum line length is set, specify that the line ending to insert should include a line feed. - public static let endLineWithLineFeed = Base64EncodingOptions(rawValue: 1 << 5) - } - - @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) - public struct Base64DecodingOptions : OptionSet, Sendable { - public let rawValue: UInt - - public init(rawValue: UInt) { - self.rawValue = rawValue - } - /// Modify the decoding algorithm so that it ignores unknown non-Base-64 bytes, including line ending characters. - public static let ignoreUnknownCharacters = Base64DecodingOptions(rawValue: 1 << 0) - } -} -#else -@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) -extension Data { - // These types are typealiased to the `NSData` options for framework builds only. - public typealias SearchOptions = NSData.SearchOptions - public typealias Base64EncodingOptions = NSData.Base64EncodingOptions - public typealias Base64DecodingOptions = NSData.Base64DecodingOptions -} -#endif //!FOUNDATION_FRAMEWORK - -extension Data.Base64EncodingOptions { - /// Use the base64url alphabet to encode the data - @available(FoundationPreview 6.3, *) - public static let base64URLAlphabet = Self(rawValue: 1 << 6) - - /// Omit the `=` padding characters in the end of the base64 encoded result - @available(FoundationPreview 6.3, *) - public static let omitPaddingCharacter = Self(rawValue: 1 << 7) -} - @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension Data : CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable { /// A human-readable description for the data. @@ -2964,8 +846,8 @@ extension Data : CustomStringConvertible, CustomDebugStringConvertible, CustomRe } // Minimal size data is output as an array - if nBytes < 64 { - children.append((label: "bytes", value: Array(self[startIndex.. { + return CollectionOfOne(self) + } +} + +extension Data { + /// Copy the contents of the data to a pointer. + /// + /// - parameter pointer: A pointer to the buffer you wish to copy the bytes into. + /// - parameter count: The number of bytes to copy. + /// - warning: This method does not verify that the contents at pointer have enough space to hold `count` bytes. + @inlinable // This is @inlinable as trivially forwarding. + public func copyBytes(to pointer: UnsafeMutablePointer, count: Int) { + precondition(count >= 0, "count of bytes to copy must not be negative") + if count == 0 { return } + _copyBytesHelper(to: UnsafeMutableRawPointer(pointer), from: startIndex..<(startIndex + count)) + } + + @inlinable // This is @inlinable as trivially forwarding. + internal func _copyBytesHelper(to pointer: UnsafeMutableRawPointer, from range: Range) { + if range.isEmpty { return } + _representation.copyBytes(to: pointer, from: range) + } + + /// Copy a subset of the contents of the data to a pointer. + /// + /// - parameter pointer: A pointer to the buffer you wish to copy the bytes into. + /// - parameter range: The range in the `Data` to copy. + /// - warning: This method does not verify that the contents at pointer have enough space to hold the required number of bytes. + @inlinable // This is @inlinable as trivially forwarding. + public func copyBytes(to pointer: UnsafeMutablePointer, from range: Range) { + _copyBytesHelper(to: pointer, from: range) + } + + // Copy the contents of the data into a buffer. + /// + /// This function copies the bytes in `range` from the data into the buffer. If the count of the `range` is greater than `MemoryLayout.stride * buffer.count` then the first N bytes will be copied into the buffer. + /// - precondition: The range must be within the bounds of the data. Otherwise `fatalError` is called. + /// - parameter buffer: A buffer to copy the data into. + /// - parameter range: A range in the data to copy into the buffer. If the range is empty, this function will return 0 without copying anything. If the range is nil, as much data as will fit into `buffer` is copied. + /// - returns: Number of bytes copied into the destination buffer. + @inlinable // This is @inlinable as generic and reasonably small. + public func copyBytes(to buffer: UnsafeMutableBufferPointer, from range: Range? = nil) -> Int { + let cnt = count + guard cnt > 0 else { return 0 } + + let copyRange : Range + if let r = range { + guard !r.isEmpty else { return 0 } + copyRange = r.lowerBound..<(r.lowerBound + Swift.min(buffer.count * MemoryLayout.stride, r.upperBound - r.lowerBound)) + } else { + copyRange = startIndex..<(startIndex + Swift.min(buffer.count * MemoryLayout.stride, cnt)) + } + + guard !copyRange.isEmpty else { return 0 } + + _copyBytesHelper(to: buffer.baseAddress!, from: copyRange) + return copyRange.upperBound - copyRange.lowerBound + } +} + //===--- DataProtocol Conditional Conformances ----------------------------===// @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) diff --git a/Sources/FoundationEssentials/Data/Data+Stub.swift b/Sources/FoundationEssentials/Data/PathOrURL.swift similarity index 64% rename from Sources/FoundationEssentials/Data/Data+Stub.swift rename to Sources/FoundationEssentials/Data/PathOrURL.swift index b449511de..0777a4465 100644 --- a/Sources/FoundationEssentials/Data/Data+Stub.swift +++ b/Sources/FoundationEssentials/Data/PathOrURL.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2018 - 2023 Apple Inc. and the Swift project authors +// Copyright (c) 2018 - 2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -10,29 +10,6 @@ // //===----------------------------------------------------------------------===// -#if !FOUNDATION_FRAMEWORK - -// Placeholder for Progress -internal final class Progress { - var completedUnitCount: Int64 - var totalUnitCount: Int64 - - init(totalUnitCount: Int64) { - self.completedUnitCount = 0 - self.totalUnitCount = totalUnitCount - } - - func becomeCurrent(withPendingUnitCount: Int64) { } - func resignCurrent() { } - var isCancelled: Bool { false } - static func current() -> Progress? { nil } - var fractionCompleted: Double { - 0.0 - } -} - -#endif // !FOUNDATION_FRAMEWORK - internal enum PathOrURL { case path(String) case url(URL) diff --git a/Sources/FoundationEssentials/Data/Representations/Data+Inline.swift b/Sources/FoundationEssentials/Data/Representations/Data+Inline.swift new file mode 100644 index 000000000..22d4bf0a9 --- /dev/null +++ b/Sources/FoundationEssentials/Data/Representations/Data+Inline.swift @@ -0,0 +1,255 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(ucrt) +import ucrt +#elseif canImport(WASILibc) +@preconcurrency import WASILibc +#elseif canImport(Bionic) +@preconcurrency import Bionic +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { + // A small inline buffer of bytes suitable for stack-allocation of small data. + // Inlinability strategy: everything here should be inlined for direct operation on the stack wherever possible. + @usableFromInline + @frozen + internal struct InlineData : Sendable { +#if _pointerBitWidth(_64) + @usableFromInline typealias Buffer = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) //len //enum + @usableFromInline var bytes: Buffer +#elseif _pointerBitWidth(_32) + @usableFromInline typealias Buffer = (UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8) //len //enum + @usableFromInline var bytes: Buffer +#else +#error ("Unsupported architecture: a definition of Buffer needs to be made with N = (MemoryLayout<(Int, Int)>.size - 2) UInt8 members to a tuple") +#endif + @usableFromInline var length: UInt8 + + @inlinable // This is @inlinable as trivially computable. + static func canStore(count: Int) -> Bool { + return count <= MemoryLayout.size + } + + static var maximumCapacity: Int { + return MemoryLayout.size + } + + @inlinable // This is @inlinable as a convenience initializer. + init(_ srcBuffer: UnsafeRawBufferPointer) { + self.init(count: srcBuffer.count) + if !srcBuffer.isEmpty { + Swift.withUnsafeMutableBytes(of: &bytes) { dstBuffer in + dstBuffer.baseAddress?.copyMemory(from: srcBuffer.baseAddress!, byteCount: srcBuffer.count) + } + } + } + + @inlinable // This is @inlinable as a trivial initializer. + init(count: Int = 0) { + assert(count <= MemoryLayout.size) +#if _pointerBitWidth(_64) + bytes = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0)) +#elseif _pointerBitWidth(_32) + bytes = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0)) +#else +#error ("Unsupported architecture: initialization for Buffer is required for this architecture") +#endif + length = UInt8(count) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(_ slice: InlineSlice, count: Int) { + self.init(count: count) + Swift.withUnsafeMutableBytes(of: &bytes) { dstBuffer in + slice.withUnsafeBytes { srcBuffer in + dstBuffer.copyMemory(from: UnsafeRawBufferPointer(start: srcBuffer.baseAddress, count: count)) + } + } + } + + @inlinable // This is @inlinable as a convenience initializer. + init(_ slice: LargeSlice, count: Int) { + self.init(count: count) + Swift.withUnsafeMutableBytes(of: &bytes) { dstBuffer in + slice.withUnsafeBytes { srcBuffer in + dstBuffer.copyMemory(from: UnsafeRawBufferPointer(start: srcBuffer.baseAddress, count: count)) + } + } + } + + @inlinable // This is @inlinable as trivially computable. + var capacity: Int { + return MemoryLayout.size + } + + @inlinable // This is @inlinable as trivially computable. + var count: Int { + get { + return Int(length) + } + set(newValue) { + assert(newValue <= MemoryLayout.size) + if newValue > length { + resetBytes(in: Int(length) ..< newValue) // Also extends length + } else { + length = UInt8(newValue) + } + } + } + + @inlinable // This is @inlinable as trivially computable. + var startIndex: Int { + return 0 + } + + @inlinable // This is @inlinable as trivially computable. + var endIndex: Int { + return count + } + + @inlinable // This is @inlinable as a generic, trivially forwarding function. + func withUnsafeBytes(_ apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { + let count = Int(length) + return try Swift.withUnsafeBytes(of: bytes) { (rawBuffer) throws -> Result in + return try apply(UnsafeRawBufferPointer(start: rawBuffer.baseAddress, count: count)) + } + } + + @inlinable // This is @inlinable as a generic, trivially forwarding function. + mutating func withUnsafeMutableBytes(_ apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { + let count = Int(length) + return try Swift.withUnsafeMutableBytes(of: &bytes) { (rawBuffer) throws -> Result in + return try apply(UnsafeMutableRawBufferPointer(start: rawBuffer.baseAddress, count: count)) + } + } + + @inlinable // This is @inlinable as trivially computable. + mutating func append(byte: UInt8) { + let count = self.count + assert(count + 1 <= MemoryLayout.size) + Swift.withUnsafeMutableBytes(of: &bytes) { $0[count] = byte } + self.length += 1 + } + + @inlinable // This is @inlinable as trivially computable. + mutating func append(contentsOf buffer: UnsafeRawBufferPointer) { + guard !buffer.isEmpty else { return } + assert(count + buffer.count <= MemoryLayout.size) + let cnt = count + _ = Swift.withUnsafeMutableBytes(of: &bytes) { rawBuffer in + rawBuffer.baseAddress?.advanced(by: cnt).copyMemory(from: buffer.baseAddress!, byteCount: buffer.count) + } + + length += UInt8(buffer.count) + } + + @inlinable // This is @inlinable as trivially computable. + subscript(index: Index) -> UInt8 { + get { + assert(index <= MemoryLayout.size) + precondition(index < length, "index \(index) is out of bounds of 0..<\(length)") + return Swift.withUnsafeBytes(of: bytes) { rawBuffer -> UInt8 in + return rawBuffer[index] + } + } + set(newValue) { + assert(index <= MemoryLayout.size) + precondition(index < length, "index \(index) is out of bounds of 0..<\(length)") + Swift.withUnsafeMutableBytes(of: &bytes) { rawBuffer in + rawBuffer[index] = newValue + } + } + } + + @inlinable // This is @inlinable as trivially computable. + mutating func resetBytes(in range: Range) { + assert(range.lowerBound <= MemoryLayout.size) + assert(range.upperBound <= MemoryLayout.size) + precondition(range.lowerBound <= length, "index \(range.lowerBound) is out of bounds of 0..<\(length)") + if length < range.upperBound { + length = UInt8(range.upperBound) + } + + let _ = Swift.withUnsafeMutableBytes(of: &bytes) { rawBuffer in + memset(rawBuffer.baseAddress!.advanced(by: range.lowerBound), 0, range.upperBound - range.lowerBound) + } + } + + @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. + mutating func replaceSubrange(_ subrange: Range, with replacementBytes: UnsafeRawPointer?, count replacementLength: Int) { + assert(subrange.lowerBound <= MemoryLayout.size) + assert(subrange.upperBound <= MemoryLayout.size) + assert(count - (subrange.upperBound - subrange.lowerBound) + replacementLength <= MemoryLayout.size) + precondition(subrange.lowerBound <= length, "index \(subrange.lowerBound) is out of bounds of 0..<\(length)") + precondition(subrange.upperBound <= length, "index \(subrange.upperBound) is out of bounds of 0..<\(length)") + let currentLength = count + let resultingLength = currentLength - (subrange.upperBound - subrange.lowerBound) + replacementLength + let shift = resultingLength - currentLength + Swift.withUnsafeMutableBytes(of: &bytes) { mutableBytes in + /* shift the trailing bytes */ + let start = subrange.lowerBound + let length = subrange.upperBound - subrange.lowerBound + if shift != 0 { + memmove(mutableBytes.baseAddress!.advanced(by: start + replacementLength), mutableBytes.baseAddress!.advanced(by: start + length), currentLength - start - length) + } + if replacementLength != 0 { + memmove(mutableBytes.baseAddress!.advanced(by: start), replacementBytes!, replacementLength) + } + } + length = UInt8(resultingLength) + } + + @inlinable // This is @inlinable as trivially computable. + func copyBytes(to pointer: UnsafeMutableRawPointer, from range: Range) { + precondition(startIndex <= range.lowerBound, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(startIndex <= range.upperBound, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(range.upperBound <= endIndex, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + + Swift.withUnsafeBytes(of: bytes) { + let cnt = Swift.min($0.count, range.upperBound - range.lowerBound) + guard cnt > 0 else { return } + pointer.copyMemory(from: $0.baseAddress!.advanced(by: range.lowerBound), byteCount: cnt) + } + } + + @inline(__always) // This should always be inlined into _Representation.hash(into:). + func hash(into hasher: inout Hasher) { + // **NOTE**: this uses `count` (an Int) and NOT `length` (a UInt8) + // Despite having the same value, they hash differently. InlineSlice and LargeSlice both use `count` (an Int); if you combine the same bytes but with `length` over `count`, you can get a different hash. + // + // This affects slices, which are InlineSlice and not InlineData: + // + // let d = Data([0xFF, 0xFF]) // InlineData + // let s = Data([0, 0xFF, 0xFF]).dropFirst() // InlineSlice + // assert(s == d) + // assert(s.hashValue == d.hashValue) + hasher.combine(count) + + Swift.withUnsafeBytes(of: bytes) { + // We have access to the full byte buffer here, but not all of it is meaningfully used (bytes past self.length may be garbage). + let bytes = UnsafeRawBufferPointer(start: $0.baseAddress, count: self.count) + hasher.combine(bytes: bytes) + } + } + } +} diff --git a/Sources/FoundationEssentials/Data/Representations/Data+InlineSlice.swift b/Sources/FoundationEssentials/Data/Representations/Data+InlineSlice.swift new file mode 100644 index 000000000..0e2abd7e6 --- /dev/null +++ b/Sources/FoundationEssentials/Data/Representations/Data+InlineSlice.swift @@ -0,0 +1,248 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { +#if _pointerBitWidth(_64) + @usableFromInline internal typealias HalfInt = Int32 +#elseif _pointerBitWidth(_32) + @usableFromInline internal typealias HalfInt = Int16 +#else +#error ("Unsupported architecture: a definition of half of the pointer sized Int needs to be defined for this architecture") +#endif + + // A buffer of bytes too large to fit in an InlineData, but still small enough to fit a storage pointer + range in two words. + // Inlinability strategy: everything here should be easily inlinable as large _DataStorage methods should not inline into here. + @usableFromInline + @frozen + internal struct InlineSlice : Sendable { + // ***WARNING*** + // These ivars are specifically laid out so that they cause the enum _Representation to be 16 bytes on 64 bit platforms. This means we _MUST_ have the class type thing last + @usableFromInline var slice: Range + @usableFromInline var storage: __DataStorage + + @inlinable // This is @inlinable as trivially computable. + static func canStore(count: Int) -> Bool { + return count < HalfInt.max + } + + @inlinable // This is @inlinable as a convenience initializer. + init(_ buffer: UnsafeRawBufferPointer) { + assert(buffer.count < HalfInt.max) + self.init(__DataStorage(bytes: buffer.baseAddress, length: buffer.count), count: buffer.count) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(capacity: Int) { + assert(capacity < HalfInt.max) + self.init(__DataStorage(capacity: capacity), count: 0) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(count: Int) { + assert(count < HalfInt.max) + self.init(__DataStorage(length: count), count: count) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(_ inline: InlineData) { + assert(inline.count < HalfInt.max) + self.init(inline.withUnsafeBytes { return __DataStorage(bytes: $0.baseAddress, length: $0.count) }, count: inline.count) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(_ inline: InlineData, range: Range) { + assert(range.lowerBound < HalfInt.max) + assert(range.upperBound < HalfInt.max) + self.init(inline.withUnsafeBytes { return __DataStorage(bytes: $0.baseAddress, length: $0.count) }, range: range) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(_ large: LargeSlice) { + assert(large.range.lowerBound < HalfInt.max) + assert(large.range.upperBound < HalfInt.max) + self.init(large.storage, range: large.range) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(_ large: LargeSlice, range: Range) { + assert(range.lowerBound < HalfInt.max) + assert(range.upperBound < HalfInt.max) + self.init(large.storage, range: range) + } + + @inlinable // This is @inlinable as a trivial initializer. + init(_ storage: __DataStorage, count: Int) { + assert(count < HalfInt.max) + self.storage = storage + slice = 0..) { + assert(range.lowerBound < HalfInt.max) + assert(range.upperBound < HalfInt.max) + self.storage = storage + slice = HalfInt(range.lowerBound).. 0 { + let additionalRange = Int(slice.upperBound) ..< Int(slice.upperBound) + difference + storage.resetBytes(in: additionalRange) // Also extends storage length + } else { + storage.length += difference + } + slice = slice.lowerBound..<(slice.lowerBound + HalfInt(newValue)) + } + } + + @inlinable // This is @inlinable as trivially computable. + var range: Range { + get { + return Int(slice.lowerBound)..(_ apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { + return try storage.withUnsafeBytes(in: range, apply: apply) + } + + @inlinable // This is @inlinable as a generic, trivially forwarding function. + mutating func withUnsafeMutableBytes(_ apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { + ensureUniqueReference() + return try storage.withUnsafeMutableBytes(in: range, apply: apply) + } + + @inlinable // This is @inlinable as reasonably small. + mutating func append(contentsOf buffer: UnsafeRawBufferPointer) { + assert(endIndex + buffer.count < HalfInt.max) + ensureUniqueReference() + storage.replaceBytes( + in: ( + location: range.upperBound, + length: storage.length - (range.upperBound - storage._offset)), + with: buffer.baseAddress, + length: buffer.count) + slice = slice.lowerBound.. UInt8 { + get { + assert(index < HalfInt.max) + precondition(startIndex <= index, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(index < endIndex, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") + return storage.get(index) + } + set(newValue) { + assert(index < HalfInt.max) + precondition(startIndex <= index, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(index < endIndex, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") + ensureUniqueReference() + storage.set(index, to: newValue) + } + } + + @inlinable // This is @inlinable as reasonably small. + mutating func resetBytes(in range: Range) { + assert(range.lowerBound < HalfInt.max) + assert(range.upperBound < HalfInt.max) + precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + ensureUniqueReference() + storage.resetBytes(in: range) + if slice.upperBound < range.upperBound { + slice = slice.lowerBound.., with bytes: UnsafeRawPointer?, count cnt: Int) { + precondition(startIndex <= subrange.lowerBound, "index \(subrange.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(subrange.lowerBound <= endIndex, "index \(subrange.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(startIndex <= subrange.upperBound, "index \(subrange.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(subrange.upperBound <= endIndex, "index \(subrange.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + + ensureUniqueReference() + let upper = range.upperBound + let nsRange = ( + location: subrange.lowerBound, + length: subrange.upperBound - subrange.lowerBound) + storage.replaceBytes(in: nsRange, with: bytes, length: cnt) + let resultingUpper = upper - (subrange.upperBound - subrange.lowerBound) + cnt + slice = slice.lowerBound..) { + precondition(startIndex <= range.lowerBound, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(startIndex <= range.upperBound, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(range.upperBound <= endIndex, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + storage.copyBytes(to: pointer, from: range) + } + + @inline(__always) // This should always be inlined into _Representation.hash(into:). + func hash(into hasher: inout Hasher) { + hasher.combine(count) + + self.withUnsafeBytes { bytes in + hasher.combine(bytes: bytes) + } + } + } +} diff --git a/Sources/FoundationEssentials/Data/Representations/Data+LargeSlice.swift b/Sources/FoundationEssentials/Data/Representations/Data+LargeSlice.swift new file mode 100644 index 000000000..2974d2921 --- /dev/null +++ b/Sources/FoundationEssentials/Data/Representations/Data+LargeSlice.swift @@ -0,0 +1,231 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { + // A reference wrapper around a Range for when the range of a data buffer is too large to whole in a single word. + // Inlinability strategy: everything should be inlinable as trivial. + @usableFromInline + @_fixed_layout + internal final class RangeReference : @unchecked Sendable { + @usableFromInline var range: Range + + @inlinable @inline(__always) // This is @inlinable as trivially forwarding. + var lowerBound: Int { + return range.lowerBound + } + + @inlinable @inline(__always) // This is @inlinable as trivially forwarding. + var upperBound: Int { + return range.upperBound + } + + @inlinable @inline(__always) // This is @inlinable as trivially computable. + var count: Int { + // The upper bound is guaranteed to be greater than or equal to the lower bound, and the lower bound must be non-negative so subtraction can never overflow + return range.upperBound &- range.lowerBound + } + + @inlinable @inline(__always) // This is @inlinable as a trivial initializer. + init(_ range: Range) { + self.range = range + } + } + + // A buffer of bytes whose range is too large to fit in a single word. Used alongside a RangeReference to make it fit into _Representation's two-word size. + // Inlinability strategy: everything here should be easily inlinable as large _DataStorage methods should not inline into here. + @usableFromInline + @frozen + internal struct LargeSlice : Sendable { + // ***WARNING*** + // These ivars are specifically laid out so that they cause the enum _Representation to be 16 bytes on 64 bit platforms. This means we _MUST_ have the class type thing last + @usableFromInline var slice: RangeReference + @usableFromInline var storage: __DataStorage + + @inlinable // This is @inlinable as a convenience initializer. + init(_ buffer: UnsafeRawBufferPointer) { + self.init(__DataStorage(bytes: buffer.baseAddress, length: buffer.count), count: buffer.count) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(capacity: Int) { + self.init(__DataStorage(capacity: capacity), count: 0) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(count: Int) { + self.init(__DataStorage(length: count), count: count) + } + + @inlinable // This is @inlinable as a convenience initializer. + init(_ inline: InlineData) { + let storage = inline.withUnsafeBytes { return __DataStorage(bytes: $0.baseAddress, length: $0.count) } + self.init(storage, count: inline.count) + } + + @inlinable // This is @inlinable as a trivial initializer. + init(_ slice: InlineSlice) { + self.storage = slice.storage + self.slice = RangeReference(slice.range) + } + + @inlinable // This is @inlinable as a trivial initializer. + init(_ storage: __DataStorage, count: Int) { + self.storage = storage + self.slice = RangeReference(0..) { + self.storage = storage + self.slice = RangeReference(range) + } + + @inlinable // This is @inlinable as trivially computable (and inlining may help avoid retain-release traffic). + mutating func ensureUniqueReference() { + if !isKnownUniquelyReferenced(&storage) { + storage = storage.mutableCopy(range) + } + if !isKnownUniquelyReferenced(&slice) { + slice = RangeReference(range) + } + } + + @inlinable // This is @inlinable as trivially forwarding. + var startIndex: Int { + return slice.range.lowerBound + } + + @inlinable // This is @inlinable as trivially forwarding. + var endIndex: Int { + return slice.range.upperBound + } + + @inlinable // This is @inlinable as trivially forwarding. + var capacity: Int { + return storage.capacity + } + + @inlinable // This is @inlinable as trivially computable. + mutating func reserveCapacity(_ minimumCapacity: Int) { + ensureUniqueReference() + // the current capacity can be zero (representing externally owned buffer), and count can be greater than the capacity + storage.ensureUniqueBufferReference(growingTo: Swift.max(minimumCapacity, count)) + } + + @inlinable // This is @inlinable as trivially computable. + var count: Int { + get { + return slice.count + } + set(newValue) { + ensureUniqueReference() + let difference = newValue - count + if difference > 0 { + let additionalRange = Int(slice.upperBound) ..< Int(slice.upperBound) + difference + storage.resetBytes(in: additionalRange) // Already sets the length + } else { + storage.length += difference + } + slice.range = slice.range.lowerBound..<(slice.range.lowerBound + newValue) + } + } + + @inlinable // This is @inlinable as it is trivially forwarding. + var range: Range { + return slice.range + } + + @inlinable // This is @inlinable as a generic, trivially forwarding function. + func withUnsafeBytes(_ apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { + return try storage.withUnsafeBytes(in: range, apply: apply) + } + + @inlinable // This is @inlinable as a generic, trivially forwarding function. + mutating func withUnsafeMutableBytes(_ apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { + ensureUniqueReference() + return try storage.withUnsafeMutableBytes(in: range, apply: apply) + } + + @inlinable // This is @inlinable as reasonably small. + mutating func append(contentsOf buffer: UnsafeRawBufferPointer) { + ensureUniqueReference() + storage.replaceBytes( + in: ( + location: range.upperBound, + length: storage.length - (range.upperBound - storage._offset)), + with: buffer.baseAddress, + length: buffer.count) + slice.range = slice.range.lowerBound.. UInt8 { + get { + precondition(startIndex <= index, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(index < endIndex, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") + return storage.get(index) + } + set(newValue) { + precondition(startIndex <= index, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(index < endIndex, "index \(index) is out of bounds of \(startIndex)..<\(endIndex)") + ensureUniqueReference() + storage.set(index, to: newValue) + } + } + + @inlinable // This is @inlinable as reasonably small. + mutating func resetBytes(in range: Range) { + precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + ensureUniqueReference() + storage.resetBytes(in: range) + if slice.range.upperBound < range.upperBound { + slice.range = slice.range.lowerBound.., with bytes: UnsafeRawPointer?, count cnt: Int) { + precondition(startIndex <= subrange.lowerBound, "index \(subrange.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(subrange.lowerBound <= endIndex, "index \(subrange.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(startIndex <= subrange.upperBound, "index \(subrange.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(subrange.upperBound <= endIndex, "index \(subrange.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + + ensureUniqueReference() + let upper = range.upperBound + let nsRange = ( + location: subrange.lowerBound, + length: subrange.upperBound - subrange.lowerBound) + storage.replaceBytes(in: nsRange, with: bytes, length: cnt) + let resultingUpper = upper - (subrange.upperBound - subrange.lowerBound) + cnt + slice.range = slice.range.lowerBound..) { + precondition(startIndex <= range.lowerBound, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(startIndex <= range.upperBound, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + precondition(range.upperBound <= endIndex, "index \(range.upperBound) is out of bounds of \(startIndex)..<\(endIndex)") + storage.copyBytes(to: pointer, from: range) + } + + @inline(__always) // This should always be inlined into _Representation.hash(into:). + func hash(into hasher: inout Hasher) { + hasher.combine(count) + + self.withUnsafeBytes { bytes in + hasher.combine(bytes: bytes) + } + } + } +} diff --git a/Sources/FoundationEssentials/Data/Representations/Data+Representation.swift b/Sources/FoundationEssentials/Data/Representations/Data+Representation.swift new file mode 100644 index 000000000..a6de6d806 --- /dev/null +++ b/Sources/FoundationEssentials/Data/Representations/Data+Representation.swift @@ -0,0 +1,562 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Data { + // The actual storage for Data's various representations. + // Inlinability strategy: almost everything should be inlinable as forwarding the underlying implementations. (Inlining can also help avoid retain-release traffic around pulling values out of enums.) + @usableFromInline + @frozen + internal enum _Representation : Sendable { + case empty + case inline(InlineData) + case slice(InlineSlice) + case large(LargeSlice) + + @inlinable // This is @inlinable as a trivial initializer. + init(_ buffer: UnsafeRawBufferPointer) { + if buffer.isEmpty { + self = .empty + } else if InlineData.canStore(count: buffer.count) { + self = .inline(InlineData(buffer)) + } else if InlineSlice.canStore(count: buffer.count) { + self = .slice(InlineSlice(buffer)) + } else { + self = .large(LargeSlice(buffer)) + } + } + + @inlinable // This is @inlinable as a trivial initializer. + init(_ buffer: UnsafeRawBufferPointer, owner: AnyObject) { + if buffer.isEmpty { + self = .empty + } else if InlineData.canStore(count: buffer.count) { + self = .inline(InlineData(buffer)) + } else { + let count = buffer.count + let storage = __DataStorage(bytes: UnsafeMutableRawPointer(mutating: buffer.baseAddress), length: count, copy: false, deallocator: { _, _ in + _fixLifetime(owner) + }, offset: 0) + if InlineSlice.canStore(count: count) { + self = .slice(InlineSlice(storage, count: count)) + } else { + self = .large(LargeSlice(storage, count: count)) + } + } + } + + @inlinable // This is @inlinable as a trivial initializer. + init(capacity: Int) { + if capacity == 0 { + self = .empty + } else if InlineData.canStore(count: capacity) { + self = .inline(InlineData()) + } else if InlineSlice.canStore(count: capacity) { + self = .slice(InlineSlice(capacity: capacity)) + } else { + self = .large(LargeSlice(capacity: capacity)) + } + } + + @inlinable // This is @inlinable as a trivial initializer. + init(count: Int) { + if count == 0 { + self = .empty + } else if InlineData.canStore(count: count) { + self = .inline(InlineData(count: count)) + } else if InlineSlice.canStore(count: count) { + self = .slice(InlineSlice(count: count)) + } else { + self = .large(LargeSlice(count: count)) + } + } + + @inlinable // This is @inlinable as a trivial initializer. + init(_ storage: __DataStorage, count: Int) { + if count == 0 { + self = .empty + } else if InlineData.canStore(count: count) { + self = .inline(storage.withUnsafeBytes(in: 0.. 0 else { return } + switch self { + case .empty: + if InlineData.canStore(count: minimumCapacity) { + self = .inline(InlineData()) + } else if InlineSlice.canStore(count: minimumCapacity) { + self = .slice(InlineSlice(capacity: minimumCapacity)) + } else { + self = .large(LargeSlice(capacity: minimumCapacity)) + } + case .inline(let inline): + guard minimumCapacity > inline.capacity else { return } + // we know we are going to be heap promoted + if InlineSlice.canStore(count: minimumCapacity) { + var slice = InlineSlice(inline) + slice.reserveCapacity(minimumCapacity) + self = .slice(slice) + } else { + var slice = LargeSlice(inline) + slice.reserveCapacity(minimumCapacity) + self = .large(slice) + } + case .slice(var slice): + guard minimumCapacity > slice.capacity else { return } + if InlineSlice.canStore(count: minimumCapacity) { + self = .empty + slice.reserveCapacity(minimumCapacity) + self = .slice(slice) + } else { + var large = LargeSlice(slice) + large.reserveCapacity(minimumCapacity) + self = .large(large) + } + case .large(var slice): + guard minimumCapacity > slice.capacity else { return } + self = .empty + slice.reserveCapacity(minimumCapacity) + self = .large(slice) + } + } + + @inlinable // This is @inlinable as reasonably small. + var count: Int { + get { + switch self { + case .empty: return 0 + case .inline(let inline): return inline.count + case .slice(let slice): return slice.count + case .large(let slice): return slice.count + } + } + set(newValue) { + // HACK: The definition of this inline function takes an inout reference to self, giving the optimizer a unique referencing guarantee. + // This allows us to avoid excessive retain-release traffic around modifying enum values, and inlining the function then avoids the additional frame. + @inline(__always) + func apply(_ representation: inout _Representation, _ newValue: Int) -> _Representation? { + switch representation { + case .empty: + if newValue == 0 { + return nil + } else if InlineData.canStore(count: newValue) { + return .inline(InlineData(count: newValue)) + } else if InlineSlice.canStore(count: newValue) { + return .slice(InlineSlice(count: newValue)) + } else { + return .large(LargeSlice(count: newValue)) + } + case .inline(var inline): + if newValue == 0 { + return .empty + } else if InlineData.canStore(count: newValue) { + guard inline.count != newValue else { return nil } + inline.count = newValue + return .inline(inline) + } else if InlineSlice.canStore(count: newValue) { + var slice = InlineSlice(inline) + slice.count = newValue + return .slice(slice) + } else { + var slice = LargeSlice(inline) + slice.count = newValue + return .large(slice) + } + case .slice(var slice): + if newValue == 0 && slice.startIndex == 0 { + return .empty + } else if slice.startIndex == 0 && InlineData.canStore(count: newValue) { + return .inline(InlineData(slice, count: newValue)) + } else if InlineSlice.canStore(count: newValue + slice.startIndex) { + guard slice.count != newValue else { return nil } + representation = .empty // TODO: remove this when mgottesman lands optimizations + slice.count = newValue + return .slice(slice) + } else { + var newSlice = LargeSlice(slice) + newSlice.count = newValue + return .large(newSlice) + } + case .large(var slice): + if newValue == 0 && slice.startIndex == 0 { + return .empty + } else if slice.startIndex == 0 && InlineData.canStore(count: newValue) { + return .inline(InlineData(slice, count: newValue)) + } else { + guard slice.count != newValue else { return nil} + representation = .empty // TODO: remove this when mgottesman lands optimizations + slice.count = newValue + return .large(slice) + } + } + } + + if let rep = apply(&self, newValue) { + self = rep + } + } + } + + @inlinable // This is @inlinable as a generic, trivially forwarding function. + func withUnsafeBytes(_ apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { + switch self { + case .empty: + let empty = InlineData() + return try empty.withUnsafeBytes(apply) + case .inline(let inline): + return try inline.withUnsafeBytes(apply) + case .slice(let slice): + return try slice.withUnsafeBytes(apply) + case .large(let slice): + return try slice.withUnsafeBytes(apply) + } + } + + @inlinable // This is @inlinable as a generic, trivially forwarding function. + mutating func withUnsafeMutableBytes(_ apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { + switch self { + case .empty: + var empty = InlineData() + return try empty.withUnsafeMutableBytes(apply) + case .inline(var inline): + defer { self = .inline(inline) } + return try inline.withUnsafeMutableBytes(apply) + case .slice(var slice): + self = .empty + defer { self = .slice(slice) } + return try slice.withUnsafeMutableBytes(apply) + case .large(var slice): + self = .empty + defer { self = .large(slice) } + return try slice.withUnsafeMutableBytes(apply) + } + } + + @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. + func enumerateBytes(_ block: (_ buffer: UnsafeBufferPointer, _ byteIndex: Index, _ stop: inout Bool) -> Void) { + switch self { + case .empty: + var stop = false + block(UnsafeBufferPointer(start: nil, count: 0), 0, &stop) + case .inline(let inline): + inline.withUnsafeBytes { + var stop = false + $0.withMemoryRebound(to: UInt8.self) { block($0, 0, &stop) } + } + case .slice(let slice): + slice.storage.enumerateBytes(in: slice.range, block) + case .large(let slice): + slice.storage.enumerateBytes(in: slice.range, block) + } + } + + @inlinable // This is @inlinable as reasonably small. + mutating func append(contentsOf buffer: UnsafeRawBufferPointer) { + switch self { + case .empty: + self = _Representation(buffer) + case .inline(var inline): + if InlineData.canStore(count: inline.count + buffer.count) { + inline.append(contentsOf: buffer) + self = .inline(inline) + } else if InlineSlice.canStore(count: inline.count + buffer.count) { + var newSlice = InlineSlice(inline) + newSlice.append(contentsOf: buffer) + self = .slice(newSlice) + } else { + var newSlice = LargeSlice(inline) + newSlice.append(contentsOf: buffer) + self = .large(newSlice) + } + case .slice(var slice): + if InlineSlice.canStore(count: slice.range.upperBound + buffer.count) { + self = .empty + defer { self = .slice(slice) } + slice.append(contentsOf: buffer) + } else { + self = .empty + var newSlice = LargeSlice(slice) + newSlice.append(contentsOf: buffer) + self = .large(newSlice) + } + case .large(var slice): + self = .empty + defer { self = .large(slice) } + slice.append(contentsOf: buffer) + } + } + + @inlinable // This is @inlinable as reasonably small. + mutating func resetBytes(in range: Range) { + switch self { + case .empty: + if range.upperBound == 0 { + self = .empty + } else if InlineData.canStore(count: range.upperBound) { + precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + self = .inline(InlineData(count: range.upperBound)) + } else if InlineSlice.canStore(count: range.upperBound) { + precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + self = .slice(InlineSlice(count: range.upperBound)) + } else { + precondition(range.lowerBound <= endIndex, "index \(range.lowerBound) is out of bounds of \(startIndex)..<\(endIndex)") + self = .large(LargeSlice(count: range.upperBound)) + } + case .inline(var inline): + if inline.count < range.upperBound { + if InlineSlice.canStore(count: range.upperBound) { + var slice = InlineSlice(inline) + slice.resetBytes(in: range) + self = .slice(slice) + } else { + var slice = LargeSlice(inline) + slice.resetBytes(in: range) + self = .large(slice) + } + } else { + inline.resetBytes(in: range) + self = .inline(inline) + } + case .slice(var slice): + if InlineSlice.canStore(count: range.upperBound) { + self = .empty + slice.resetBytes(in: range) + self = .slice(slice) + } else { + self = .empty + var newSlice = LargeSlice(slice) + newSlice.resetBytes(in: range) + self = .large(newSlice) + } + case .large(var slice): + self = .empty + slice.resetBytes(in: range) + self = .large(slice) + } + } + + @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. + mutating func replaceSubrange(_ subrange: Range, with bytes: UnsafeRawPointer?, count cnt: Int) { + switch self { + case .empty: + precondition(subrange.lowerBound == 0 && subrange.upperBound == 0, "range \(subrange) out of bounds of 0..<0") + if cnt == 0 { + return + } else if InlineData.canStore(count: cnt) { + self = .inline(InlineData(UnsafeRawBufferPointer(start: bytes, count: cnt))) + } else if InlineSlice.canStore(count: cnt) { + self = .slice(InlineSlice(UnsafeRawBufferPointer(start: bytes, count: cnt))) + } else { + self = .large(LargeSlice(UnsafeRawBufferPointer(start: bytes, count: cnt))) + } + case .inline(var inline): + let resultingCount = inline.count + cnt - (subrange.upperBound - subrange.lowerBound) + if resultingCount == 0 { + self = .empty + } else if InlineData.canStore(count: resultingCount) { + inline.replaceSubrange(subrange, with: bytes, count: cnt) + self = .inline(inline) + } else if InlineSlice.canStore(count: resultingCount) { + var slice = InlineSlice(inline) + slice.replaceSubrange(subrange, with: bytes, count: cnt) + self = .slice(slice) + } else { + var slice = LargeSlice(inline) + slice.replaceSubrange(subrange, with: bytes, count: cnt) + self = .large(slice) + } + case .slice(var slice): + let resultingUpper = slice.endIndex + cnt - (subrange.upperBound - subrange.lowerBound) + if slice.startIndex == 0 && resultingUpper == 0 { + self = .empty + } else if slice.startIndex == 0 && InlineData.canStore(count: resultingUpper) { + self = .empty + slice.replaceSubrange(subrange, with: bytes, count: cnt) + self = .inline(InlineData(slice, count: slice.count)) + } else if InlineSlice.canStore(count: resultingUpper) { + self = .empty + slice.replaceSubrange(subrange, with: bytes, count: cnt) + self = .slice(slice) + } else { + self = .empty + var newSlice = LargeSlice(slice) + newSlice.replaceSubrange(subrange, with: bytes, count: cnt) + self = .large(newSlice) + } + case .large(var slice): + let resultingUpper = slice.endIndex + cnt - (subrange.upperBound - subrange.lowerBound) + if slice.startIndex == 0 && resultingUpper == 0 { + self = .empty + } else if slice.startIndex == 0 && InlineData.canStore(count: resultingUpper) { + var inline = InlineData(count: resultingUpper) + inline.withUnsafeMutableBytes { inlineBuffer in + if cnt > 0 { + inlineBuffer.baseAddress?.advanced(by: subrange.lowerBound).copyMemory(from: bytes!, byteCount: cnt) + } + slice.withUnsafeBytes { buffer in + if subrange.lowerBound > 0 { + inlineBuffer.baseAddress?.copyMemory(from: buffer.baseAddress!, byteCount: subrange.lowerBound) + } + if subrange.upperBound < resultingUpper { + inlineBuffer.baseAddress?.advanced(by: subrange.upperBound).copyMemory(from: buffer.baseAddress!.advanced(by: subrange.upperBound), byteCount: resultingUpper - subrange.upperBound) + } + } + } + self = .inline(inline) + } else if InlineSlice.canStore(count: slice.startIndex) && InlineSlice.canStore(count: resultingUpper) { + self = .empty + var newSlice = InlineSlice(slice) + newSlice.replaceSubrange(subrange, with: bytes, count: cnt) + self = .slice(newSlice) + } else { + self = .empty + slice.replaceSubrange(subrange, with: bytes, count: cnt) + self = .large(slice) + } + } + } + + @inlinable // This is @inlinable as trivially forwarding. + subscript(index: Index) -> UInt8 { + get { + switch self { + case .empty: preconditionFailure("index \(index) out of range of 0") + case .inline(let inline): return inline[index] + case .slice(let slice): return slice[index] + case .large(let slice): return slice[index] + } + } + set(newValue) { + switch self { + case .empty: preconditionFailure("index \(index) out of range of 0") + case .inline(var inline): + inline[index] = newValue + self = .inline(inline) + case .slice(var slice): + self = .empty + slice[index] = newValue + self = .slice(slice) + case .large(var slice): + self = .empty + slice[index] = newValue + self = .large(slice) + } + } + } + + @inlinable // This is @inlinable as reasonably small. + subscript(bounds: Range) -> Data { + get { + switch self { + case .empty: + precondition(bounds.lowerBound == 0 && (bounds.upperBound - bounds.lowerBound) == 0, "Range \(bounds) out of bounds 0..<0") + return Data() + case .inline(let inline): + precondition(bounds.upperBound <= inline.count, "Range \(bounds) out of bounds 0..<\(inline.count)") + if bounds.lowerBound == 0 { + var newInline = inline + newInline.count = bounds.upperBound + return Data(representation: .inline(newInline)) + } else { + return Data(representation: .slice(InlineSlice(inline, range: bounds))) + } + case .slice(let slice): + precondition(slice.startIndex <= bounds.lowerBound, "Range \(bounds) out of bounds \(slice.range)") + precondition(bounds.lowerBound <= slice.endIndex, "Range \(bounds) out of bounds \(slice.range)") + precondition(slice.startIndex <= bounds.upperBound, "Range \(bounds) out of bounds \(slice.range)") + precondition(bounds.upperBound <= slice.endIndex, "Range \(bounds) out of bounds \(slice.range)") + if bounds.lowerBound == 0 && bounds.upperBound == 0 { + return Data() + } else if bounds.lowerBound == 0 && InlineData.canStore(count: bounds.count) { + return Data(representation: .inline(InlineData(slice, count: bounds.count))) + } else { + var newSlice = slice + newSlice.range = bounds + return Data(representation: .slice(newSlice)) + } + case .large(let slice): + precondition(slice.startIndex <= bounds.lowerBound, "Range \(bounds) out of bounds \(slice.range)") + precondition(bounds.lowerBound <= slice.endIndex, "Range \(bounds) out of bounds \(slice.range)") + precondition(slice.startIndex <= bounds.upperBound, "Range \(bounds) out of bounds \(slice.range)") + precondition(bounds.upperBound <= slice.endIndex, "Range \(bounds) out of bounds \(slice.range)") + if bounds.lowerBound == 0 && bounds.upperBound == 0 { + return Data() + } else if bounds.lowerBound == 0 && InlineData.canStore(count: bounds.upperBound) { + return Data(representation: .inline(InlineData(slice, count: bounds.upperBound))) + } else if InlineSlice.canStore(count: bounds.lowerBound) && InlineSlice.canStore(count: bounds.upperBound) { + return Data(representation: .slice(InlineSlice(slice, range: bounds))) + } else { + var newSlice = slice + newSlice.slice = RangeReference(bounds) + return Data(representation: .large(newSlice)) + } + } + } + } + + @inlinable // This is @inlinable as trivially forwarding. + var startIndex: Int { + switch self { + case .empty: return 0 + case .inline: return 0 + case .slice(let slice): return slice.startIndex + case .large(let slice): return slice.startIndex + } + } + + @inlinable // This is @inlinable as trivially forwarding. + var endIndex: Int { + switch self { + case .empty: return 0 + case .inline(let inline): return inline.count + case .slice(let slice): return slice.endIndex + case .large(let slice): return slice.endIndex + } + } + + @inlinable // This is @inlinable as trivially forwarding. + func copyBytes(to pointer: UnsafeMutableRawPointer, from range: Range) { + switch self { + case .empty: + precondition(range.lowerBound == 0 && range.upperBound == 0, "Range \(range) out of bounds 0..<0") + return + case .inline(let inline): + inline.copyBytes(to: pointer, from: range) + case .slice(let slice): + slice.copyBytes(to: pointer, from: range) + case .large(let slice): + slice.copyBytes(to: pointer, from: range) + } + } + + @inline(__always) // This should always be inlined into Data.hash(into:). + func hash(into hasher: inout Hasher) { + switch self { + case .empty: + hasher.combine(0) + case .inline(let inline): + inline.hash(into: &hasher) + case .slice(let slice): + slice.hash(into: &hasher) + case .large(let large): + large.hash(into: &hasher) + } + } + } +} diff --git a/Sources/FoundationEssentials/Data/Representations/DataStorage.swift b/Sources/FoundationEssentials/Data/Representations/DataStorage.swift new file mode 100644 index 000000000..146da906a --- /dev/null +++ b/Sources/FoundationEssentials/Data/Representations/DataStorage.swift @@ -0,0 +1,517 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(ucrt) +import ucrt +#elseif canImport(WASILibc) +@preconcurrency import WASILibc +#elseif canImport(Bionic) +@preconcurrency import Bionic +#endif + +// Underlying storage representation for medium and large data. +// Inlinability strategy: methods from here should not inline into InlineSlice or LargeSlice unless trivial. +// NOTE: older overlays called this class _DataStorage. The two must +// coexist without a conflicting ObjC class name, so it was renamed. +// The old name must not be used in the new runtime. +@usableFromInline +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +internal final class __DataStorage : @unchecked Sendable { + @usableFromInline static let maxSize = Int.max >> 1 + @usableFromInline static let vmOpsThreshold = Platform.pageSize * 4 + + static func allocate(_ size: Int, _ clear: Bool) -> UnsafeMutableRawPointer? { +#if canImport(Darwin) && _pointerBitWidth(_64) && !NO_TYPED_MALLOC + var typeDesc = malloc_type_descriptor_v0_t() + typeDesc.summary.layout_semantics.contains_generic_data = true + if clear { + return malloc_type_calloc(1, size, typeDesc.type_id); + } else { + return malloc_type_malloc(size, typeDesc.type_id); + } +#else + if clear { + return calloc(1, size) + } else { + return malloc(size) + } +#endif + } + + static func reallocate(_ ptr: UnsafeMutableRawPointer, _ newSize: Int) -> UnsafeMutableRawPointer? { +#if canImport(Darwin) && _pointerBitWidth(_64) && !NO_TYPED_MALLOC + var typeDesc = malloc_type_descriptor_v0_t() + typeDesc.summary.layout_semantics.contains_generic_data = true + return malloc_type_realloc(ptr, newSize, typeDesc.type_id); +#else + return realloc(ptr, newSize) +#endif + } + + @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. + static func move(_ dest_: UnsafeMutableRawPointer, _ source_: UnsafeRawPointer?, _ num_: Int) { + var dest = dest_ + var source = source_ + var num = num_ + if __DataStorage.vmOpsThreshold <= num && ((unsafeBitCast(source, to: Int.self) | Int(bitPattern: dest)) & (Platform.pageSize - 1)) == 0 { + let pages = Platform.roundDownToMultipleOfPageSize(num) + Platform.copyMemoryPages(source!, dest, pages) + source = source!.advanced(by: pages) + dest = dest.advanced(by: pages) + num -= pages + } + if num > 0 { + memmove(dest, source!, num) + } + } + + @inlinable // This is @inlinable as trivially forwarding, and does not escape the _DataStorage boundary layer. + static func shouldAllocateCleared(_ size: Int) -> Bool { + return (size > (128 * 1024)) + } + + @usableFromInline var _bytes: UnsafeMutableRawPointer? + @usableFromInline var _length: Int + @usableFromInline var _capacity: Int + @usableFromInline var _offset: Int + @usableFromInline var _deallocator: ((UnsafeMutableRawPointer, Int) -> Void)? + @usableFromInline var _needToZero: Bool + + @inlinable // This is @inlinable as trivially computable. + var bytes: UnsafeRawPointer? { + return UnsafeRawPointer(_bytes)?.advanced(by: -_offset) + } + + @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is generic and trivially forwarding. + @discardableResult + func withUnsafeBytes(in range: Range, apply: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result { + return try apply(UnsafeRawBufferPointer(start: _bytes?.advanced(by: range.lowerBound - _offset), count: Swift.min(range.upperBound - range.lowerBound, _length))) + } + + @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is generic and trivially forwarding. + @discardableResult + func withUnsafeMutableBytes(in range: Range, apply: (UnsafeMutableRawBufferPointer) throws -> Result) rethrows -> Result { + return try apply(UnsafeMutableRawBufferPointer(start: _bytes!.advanced(by:range.lowerBound - _offset), count: Swift.min(range.upperBound - range.lowerBound, _length))) + } + + @inlinable // This is @inlinable as trivially computable. + var mutableBytes: UnsafeMutableRawPointer? { + return _bytes?.advanced(by: _offset &* -1) // _offset is guaranteed to be non-negative, so it can never overflow when negating + } + + @inlinable + static var copyWillRetainMask: Int { +#if _pointerBitWidth(_64) + return Int(bitPattern: 0x8000000000000000) +#elseif _pointerBitWidth(_32) + return Int(bitPattern: 0x80000000) +#endif + } + + @inlinable + static var capacityMask: Int { +#if _pointerBitWidth(_64) + return Int(bitPattern: 0x7FFFFFFFFFFFFFFF) +#elseif _pointerBitWidth(_32) + return Int(bitPattern: 0x7FFFFFFF) +#endif + } + + @inlinable // This is @inlinable as trivially computable. + var capacity: Int { + return _capacity & __DataStorage.capacityMask + } + + @inlinable + var _copyWillRetain: Bool { + get { + return _capacity & __DataStorage.copyWillRetainMask == 0 + } + set { + if !newValue { + _capacity |= __DataStorage.copyWillRetainMask + } else { + _capacity &= __DataStorage.capacityMask + } + } + } + + @inlinable // This is @inlinable as trivially computable. + var length: Int { + get { + return _length + } + set { + setLength(newValue) + } + } + + @inlinable // This is inlinable as trivially computable. + var isExternallyOwned: Bool { + // all __DataStorages will have some sort of capacity, because empty cases hit the .empty enum _Representation + // anything with 0 capacity means that we have not allocated this pointer and consequently mutation is not ours to make. + return _capacity == 0 + } + + @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. + func ensureUniqueBufferReference(growingTo newLength: Int = 0, clear: Bool = false) { + guard isExternallyOwned || newLength > _capacity else { return } + + if newLength == 0 { + if isExternallyOwned { + let newCapacity = malloc_good_size(_length) + let newBytes = __DataStorage.allocate(newCapacity, false) + __DataStorage.move(newBytes!, _bytes!, _length) + _freeBytes() + _bytes = newBytes + _capacity = newCapacity + _needToZero = false + } + } else if isExternallyOwned { + let newCapacity = malloc_good_size(newLength) + let newBytes = __DataStorage.allocate(newCapacity, clear) + if let bytes = _bytes { + __DataStorage.move(newBytes!, bytes, _length) + } + _freeBytes() + _bytes = newBytes + _capacity = newCapacity + _length = newLength + _needToZero = true + } else { + let cap = _capacity + var additionalCapacity = (newLength >> (__DataStorage.vmOpsThreshold <= newLength ? 2 : 1)) + if Int.max - additionalCapacity < newLength { + additionalCapacity = 0 + } + var newCapacity = malloc_good_size(Swift.max(cap, newLength + additionalCapacity)) + let origLength = _length + var allocateCleared = clear && __DataStorage.shouldAllocateCleared(newCapacity) + var newBytes: UnsafeMutableRawPointer? = nil + if _bytes == nil { + newBytes = __DataStorage.allocate(newCapacity, allocateCleared) + if newBytes == nil { + /* Try again with minimum length */ + allocateCleared = clear && __DataStorage.shouldAllocateCleared(newLength) + newBytes = __DataStorage.allocate(newLength, allocateCleared) + } + } else { + let tryCalloc = (origLength == 0 || (newLength / origLength) >= 4) + if allocateCleared && tryCalloc { + newBytes = __DataStorage.allocate(newCapacity, true) + if let newBytes = newBytes { + __DataStorage.move(newBytes, _bytes!, origLength) + _freeBytes() + } + } + /* Where calloc/memmove/free fails, realloc might succeed */ + if newBytes == nil { + allocateCleared = false + if _deallocator != nil { + newBytes = __DataStorage.allocate(newCapacity, true) + if let newBytes = newBytes { + __DataStorage.move(newBytes, _bytes!, origLength) + _freeBytes() + } + } else { + newBytes = __DataStorage.reallocate(_bytes!, newCapacity) + } + } + /* Try again with minimum length */ + if newBytes == nil { + newCapacity = malloc_good_size(newLength) + allocateCleared = clear && __DataStorage.shouldAllocateCleared(newCapacity) + if allocateCleared && tryCalloc { + newBytes = __DataStorage.allocate(newCapacity, true) + if let newBytes = newBytes { + __DataStorage.move(newBytes, _bytes!, origLength) + _freeBytes() + } + } + if newBytes == nil { + allocateCleared = false + newBytes = __DataStorage.reallocate(_bytes!, newCapacity) + } + } + } + + if newBytes == nil { + /* Could not allocate bytes */ + // At this point if the allocation cannot occur the process is likely out of memory + // and Bad-Thingsā„¢ are going to happen anyhow + fatalError("unable to allocate memory for length (\(newLength))") + } + + if origLength < newLength && clear && !allocateCleared { + _ = memset(newBytes!.advanced(by: origLength), 0, newLength - origLength) + } + + /* _length set by caller */ + _bytes = newBytes + _capacity = newCapacity + /* Realloc/memset doesn't zero out the entire capacity, so we must be safe and clear next time we grow the length */ + _needToZero = !allocateCleared + } + } + + func _freeBytes() { + if let bytes = _bytes { + if let dealloc = _deallocator { + dealloc(bytes, length) + } else { + free(bytes) + } + } + _deallocator = nil + } + + @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is trivially computed. + func enumerateBytes(in range: Range, _ block: (_ buffer: UnsafeBufferPointer, _ byteIndex: Data.Index, _ stop: inout Bool) -> Void) { + var stopv: Bool = false + let buffer = UnsafeRawBufferPointer(start: _bytes, count: Swift.min(range.upperBound - range.lowerBound, _length)) + buffer.withMemoryRebound(to: UInt8.self) { block($0, 0, &stopv) } + } + + @inlinable // This is @inlinable as it does not escape the _DataStorage boundary layer. + func setLength(_ length: Int) { + let origLength = _length + let newLength = length + if capacity < newLength || _bytes == nil { + ensureUniqueBufferReference(growingTo: newLength, clear: true) + } else if origLength < newLength && _needToZero { + _ = memset(_bytes! + origLength, 0, newLength - origLength) + } else if newLength < origLength { + _needToZero = true + } + _length = newLength + } + + @inlinable // This is @inlinable as it does not escape the _DataStorage boundary layer. + func append(_ bytes: UnsafeRawPointer, length: Int) { + precondition(length >= 0, "Length of appending bytes must not be negative") + let origLength = _length + let newLength = origLength + length + if capacity < newLength || _bytes == nil { + ensureUniqueBufferReference(growingTo: newLength, clear: false) + } + _length = newLength + __DataStorage.move(_bytes!.advanced(by: origLength), bytes, length) + } + + @inlinable // This is @inlinable despite escaping the __DataStorage boundary layer because it is trivially computed. + func get(_ index: Int) -> UInt8 { + // index must have already been validated by the caller + return _bytes!.load(fromByteOffset: index - _offset, as: UInt8.self) + } + + @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is trivially computed. + func set(_ index: Int, to value: UInt8) { + // index must have already been validated by the caller + ensureUniqueBufferReference() + _bytes!.storeBytes(of: value, toByteOffset: index - _offset, as: UInt8.self) + } + + @inlinable // This is @inlinable despite escaping the _DataStorage boundary layer because it is trivially computed. + func copyBytes(to pointer: UnsafeMutableRawPointer, from range: Range) { + let offsetPointer = UnsafeRawBufferPointer(start: _bytes?.advanced(by: range.lowerBound - _offset), count: Swift.min(range.upperBound - range.lowerBound, _length)) + UnsafeMutableRawBufferPointer(start: pointer, count: range.upperBound - range.lowerBound).copyMemory(from: offsetPointer) + } + + // This was an ABI entrypoint added in macOS 14-aligned releases in an attempt to work around the original declaration using NSRange instead of Range + // Using this entrypoint from existing inlinable code required an availability check, and that check has proved to be extremely expensive + // This entrypoint is left to preserve ABI compatibility, but inlinable code has since switched back to calling the original entrypoint using a tuple that is layout-compatible with NSRange + @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) + @usableFromInline + func replaceBytes(in range_: Range, with replacementBytes: UnsafeRawPointer?, length replacementLength: Int) { + // Call through to the main implementation + self.replaceBytes(in: (range_.lowerBound, range_.upperBound &- range_.lowerBound), with: replacementBytes, length: replacementLength) + } + + // This ABI entrypoint was original written using NSRange instead of Range. The ABI contract of this function must continue to accept NSRange values from code inlined into callers + // To avoid using the real NSRange type at the source level, we use a tuple that is layout-compatible with NSRange instead and use @_silgen_name to preserve the original symbol name that includes "NSRange" + @usableFromInline + @_silgen_name("$s10Foundation13__DataStorageC12replaceBytes2in4with6lengthySo8_NSRangeV_SVSgSitF") + internal func replaceBytes(in range_: (location: Int, length: Int), with replacementBytes: UnsafeRawPointer?, length replacementLength: Int) { + let range = (location: range_.location - _offset, length: range_.length) + let currentLength = _length + let resultingLength = currentLength - range.length + replacementLength + let shift = resultingLength - currentLength + let mutableBytes: UnsafeMutableRawPointer + if resultingLength > currentLength { + ensureUniqueBufferReference(growingTo: resultingLength) + _length = resultingLength + } else { + ensureUniqueBufferReference() + } + mutableBytes = _bytes! + /* shift the trailing bytes */ + let start = range.location + let length = range.length + if shift != 0 { + memmove(mutableBytes + start + replacementLength, mutableBytes + start + length, currentLength - start - length) + } + if replacementLength != 0 { + if let replacementBytes = replacementBytes { + memmove(mutableBytes + start, replacementBytes, replacementLength) + } else { + _ = memset(mutableBytes + start, 0, replacementLength) + } + } + + if resultingLength < currentLength { + setLength(resultingLength) + } + } + + @usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function. + func resetBytes(in range_: Range) { + let range = range_.lowerBound - _offset ..< range_.upperBound - _offset + if range.upperBound - range.lowerBound == 0 { return } + if _length < range.upperBound { + if capacity <= range.upperBound { + ensureUniqueBufferReference(growingTo: range.upperBound, clear: false) + } + _length = range.upperBound + } else { + ensureUniqueBufferReference() + } + _ = memset(_bytes!.advanced(by: range.lowerBound), 0, range.upperBound - range.lowerBound) + } + + @usableFromInline // This is not @inlinable as a non-trivial, non-convenience initializer. + init(length: Int) { + precondition(length < __DataStorage.maxSize) + var capacity = (length < 1024 * 1024 * 1024) ? length + (length >> 2) : length + if __DataStorage.vmOpsThreshold <= capacity { + capacity = Platform.roundUpToMultipleOfPageSize(capacity) + } + + let clear = __DataStorage.shouldAllocateCleared(length) + _bytes = __DataStorage.allocate(capacity, clear)! + _capacity = capacity + _needToZero = !clear + _length = 0 + _offset = 0 + setLength(length) + } + + @usableFromInline // This is not @inlinable as a non-convenience initializer. + init(capacity capacity_: Int = 0) { + var capacity = capacity_ + precondition(capacity < __DataStorage.maxSize) + if __DataStorage.vmOpsThreshold <= capacity { + capacity = Platform.roundUpToMultipleOfPageSize(capacity) + } + _length = 0 + _bytes = __DataStorage.allocate(capacity, false)! + _capacity = capacity + _needToZero = true + _offset = 0 + } + + @usableFromInline // This is not @inlinable as a non-convenience initializer. + init(bytes: UnsafeRawPointer?, length: Int) { + precondition(length < __DataStorage.maxSize) + _offset = 0 + if length == 0 { + _capacity = 0 + _length = 0 + _needToZero = false + _bytes = nil + } else if __DataStorage.vmOpsThreshold <= length { + _capacity = length + _length = length + _needToZero = true + _bytes = __DataStorage.allocate(length, false)! + __DataStorage.move(_bytes!, bytes, length) + } else { + var capacity = length + if __DataStorage.vmOpsThreshold <= capacity { + capacity = Platform.roundUpToMultipleOfPageSize(capacity) + } + _length = length + _bytes = __DataStorage.allocate(capacity, false)! + _capacity = capacity + _needToZero = true + __DataStorage.move(_bytes!, bytes, length) + } + } + + @usableFromInline // This is not @inlinable as a non-convenience initializer. + init(bytes: UnsafeMutableRawPointer?, length: Int, copy: Bool, deallocator: ((UnsafeMutableRawPointer, Int) -> Void)?, offset: Int) { + precondition(length < __DataStorage.maxSize) + _offset = offset + if length == 0 { + _capacity = 0 + _length = 0 + _needToZero = false + _bytes = nil + if let dealloc = deallocator, + let bytes_ = bytes { + dealloc(bytes_, length) + } + } else if !copy { + _capacity = length + _length = length + _needToZero = false + _bytes = bytes + _deallocator = deallocator + } else if __DataStorage.vmOpsThreshold <= length { + _capacity = length + _length = length + _needToZero = true + _bytes = __DataStorage.allocate(length, false)! + __DataStorage.move(_bytes!, bytes, length) + if let dealloc = deallocator { + dealloc(bytes!, length) + } + } else { + var capacity = length + if __DataStorage.vmOpsThreshold <= capacity { + capacity = Platform.roundUpToMultipleOfPageSize(capacity) + } + _length = length + _bytes = __DataStorage.allocate(capacity, false)! + _capacity = capacity + _needToZero = true + __DataStorage.move(_bytes!, bytes, length) + if let dealloc = deallocator { + dealloc(bytes!, length) + } + } + } + + @usableFromInline + init(offset: Int, bytes: UnsafeMutableRawPointer, capacity: Int, needToZero: Bool, length: Int, deallocator: ((UnsafeMutableRawPointer, Int) -> Void)?) { + _offset = offset + _bytes = bytes + _capacity = capacity + _needToZero = needToZero + _length = length + _deallocator = deallocator + } + + deinit { + _freeBytes() + } + + @inlinable // This is @inlinable despite escaping the __DataStorage boundary layer because it is trivially computed. + func mutableCopy(_ range: Range) -> __DataStorage { + return __DataStorage(bytes: _bytes?.advanced(by: range.lowerBound - _offset), length: range.upperBound - range.lowerBound, copy: true, deallocator: nil, offset: range.lowerBound) + } +} diff --git a/Sources/FoundationEssentials/Progress+Stub.swift b/Sources/FoundationEssentials/Progress+Stub.swift new file mode 100644 index 000000000..c6d2280ac --- /dev/null +++ b/Sources/FoundationEssentials/Progress+Stub.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if !FOUNDATION_FRAMEWORK + +// Placeholder for Progress +internal final class Progress { + var completedUnitCount: Int64 + var totalUnitCount: Int64 + + init(totalUnitCount: Int64) { + self.completedUnitCount = 0 + self.totalUnitCount = totalUnitCount + } + + func becomeCurrent(withPendingUnitCount: Int64) { } + func resignCurrent() { } + var isCancelled: Bool { false } + static func current() -> Progress? { nil } + var fractionCompleted: Double { + 0.0 + } +} + +#endif // !FOUNDATION_FRAMEWORK diff --git a/Sources/FoundationEssentials/ProgressManager/CMakeLists.txt b/Sources/FoundationEssentials/ProgressManager/CMakeLists.txt new file mode 100644 index 000000000..325b81e2b --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/CMakeLists.txt @@ -0,0 +1,23 @@ +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift open source project +## +## Copyright (c) 2025 Apple Inc. and the Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.md for the list of Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## +target_sources(FoundationEssentials PRIVATE + ProgressFraction.swift + ProgressManager.swift + ProgressManager+Interop.swift + ProgressManager+Properties+Accessors.swift + ProgressManager+Properties+Definitions.swift + ProgressManager+Properties+Helpers.swift + ProgressManager+State.swift + ProgressReporter.swift + Subprogress.swift) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift b/Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift new file mode 100644 index 000000000..09372289c --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift @@ -0,0 +1,345 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +internal import _ForSwiftFoundation +#endif + +internal struct ProgressFraction : Sendable, Equatable, CustomDebugStringConvertible { + var completed : Int + var total : Int? + /// Indicates whether mathematical operations on this fraction have exceeded integer limits, + /// causing the fraction to fall back to floating-point representation for accuracy. + private(set) var overflowed : Bool + + init() { + completed = 0 + total = nil + overflowed = false + } + + init(double: Double, overflow: Bool = false) { + if double == 0 { + self.completed = 0 + self.total = 1 + } else if double == 1 { + self.completed = 1 + self.total = 1 + } else { + (self.completed, self.total) = ProgressFraction._fromDouble(double) + } + self.overflowed = overflow + } + + init(completed: Int, total: Int?) { + self.total = total + self.completed = completed + self.overflowed = false + } + + // ---- + +#if FOUNDATION_FRAMEWORK + // Glue code for _NSProgressFraction and ProgressFraction + init(nsProgressFraction: _NSProgressFraction) { + self.init(completed: Int(nsProgressFraction.completed), total: Int(nsProgressFraction.total)) + } +#endif + + internal mutating func simplify() { + guard let total = self.total, total != 0 else { + return + } + + (self.completed, self.total) = ProgressFraction._simplify(completed, total) + } + + internal func simplified() -> ProgressFraction? { + if let total = self.total { + let simplified = ProgressFraction._simplify(completed, total) + return ProgressFraction(completed: simplified.0, total: simplified.1) + } else { + return nil + } + } + + /// A closure that performs floating-point arithmetic operations + private typealias FloatingPointOperation = (_ lhs: Double, _ rhs: Double) -> Double + + /// A closure that performs integer arithmetic operations with overflow detection + private typealias OverflowReportingOperation = (_ lhs: Int, _ rhs: Int) -> (Int, overflow: Bool) + + static private func _math(lhs: ProgressFraction, rhs: ProgressFraction, operation: FloatingPointOperation, overflowOperation: OverflowReportingOperation) -> ProgressFraction { + // Mathematically, it is nonsense to add or subtract something with a denominator of 0. However, for the purposes of implementing Progress' fractions, we just assume that a zero-denominator fraction is "weightless" and return the other value. We still need to check for the case where they are both nonsense though. + precondition(!(lhs.total == 0 && rhs.total == 0), "Attempt to add or subtract invalid fraction") + guard let lhsTotal = lhs.total, lhsTotal != 0 else { + return rhs + } + guard let rhsTotal = rhs.total, rhsTotal != 0 else { + return lhs + } + + guard !lhs.overflowed && !rhs.overflowed else { + // If either has overflowed already, we preserve that + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + + if let lcm = _leastCommonMultiple(lhsTotal, rhsTotal) { + let result = overflowOperation(lhs.completed * (lcm / lhsTotal), rhs.completed * (lcm / rhsTotal)) + if result.overflow { + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } else { + return ProgressFraction(completed: result.0, total: lcm) + } + } else { + // Overflow - simplify and then try again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + guard let lhsSimplified = lhsSimplified, + let rhsSimplified = rhsSimplified, + let lhsSimplifiedTotal = lhsSimplified.total, + let rhsSimplifiedTotal = rhsSimplified.total else { + // Simplification failed, fall back to double math + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + + if let lcm = _leastCommonMultiple(lhsSimplifiedTotal, rhsSimplifiedTotal) { + let result = overflowOperation(lhsSimplified.completed * (lcm / lhsSimplifiedTotal), rhsSimplified.completed * (lcm / rhsSimplifiedTotal)) + if result.overflow { + // Use original lhs/rhs here + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } else { + return ProgressFraction(completed: result.0, total: lcm) + } + } else { + // Still overflow + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + } + } + + static internal func +(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction { + return _math(lhs: lhs, rhs: rhs, operation: +, overflowOperation: { $0.addingReportingOverflow($1) }) + } + + static internal func -(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction { + return _math(lhs: lhs, rhs: rhs, operation: -, overflowOperation: { $0.subtractingReportingOverflow($1) }) + } + + static internal func *(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction? { + guard !lhs.overflowed && !rhs.overflowed else { + // If either has overflowed already, we preserve that + return ProgressFraction(double: lhs.fractionCompleted * rhs.fractionCompleted, overflow: true) + } + + guard let lhsTotal = lhs.total, let rhsTotal = rhs.total else { + return nil + } + + let newCompleted = lhs.completed.multipliedReportingOverflow(by: rhs.completed) + let newTotal = lhsTotal.multipliedReportingOverflow(by: rhsTotal) + + if newCompleted.overflow || newTotal.overflow { + // Try simplifying, then do it again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + guard let lhsSimplified = lhsSimplified, + let rhsSimplified = rhsSimplified, + let lhsSimplifiedTotal = lhsSimplified.total, + let rhsSimplifiedTotal = rhsSimplified.total else { + return nil + } + + let newCompletedSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplified.completed) + let newTotalSimplified = lhsSimplifiedTotal.multipliedReportingOverflow(by: rhsSimplifiedTotal) + + if newCompletedSimplified.overflow || newTotalSimplified.overflow { + // Still overflow + return ProgressFraction(double: lhs.fractionCompleted * rhs.fractionCompleted, overflow: true) + } else { + return ProgressFraction(completed: newCompletedSimplified.0, total: newTotalSimplified.0) + } + } else { + return ProgressFraction(completed: newCompleted.0, total: newTotal.0) + } + } + + static internal func /(lhs: ProgressFraction, rhs: Int) -> ProgressFraction? { + guard !lhs.overflowed else { + // If lhs has overflowed, we preserve that + return ProgressFraction(double: lhs.fractionCompleted / Double(rhs), overflow: true) + } + + guard let lhsTotal = lhs.total else { + return nil + } + + let newTotal = lhsTotal.multipliedReportingOverflow(by: rhs) + + if newTotal.overflow { + let simplified = lhs.simplified() + + guard let simplified = simplified, + let simplifiedTotal = simplified.total else { + return nil + } + + let newTotalSimplified = simplifiedTotal.multipliedReportingOverflow(by: rhs) + + if newTotalSimplified.overflow { + // Still overflow + return ProgressFraction(double: lhs.fractionCompleted / Double(rhs), overflow: true) + } else { + return ProgressFraction(completed: lhs.completed, total: newTotalSimplified.0) + } + } else { + return ProgressFraction(completed: lhs.completed, total: newTotal.0) + } + } + + static internal func ==(lhs: ProgressFraction, rhs: ProgressFraction) -> Bool { + if lhs.isNaN || rhs.isNaN { + // NaN fractions are never equal + return false + } else if lhs.total == rhs.total { + // Direct comparison of numerator + return lhs.completed == rhs.completed + } else if lhs.total == nil && rhs.total != nil { + return false + } else if lhs.total != nil && rhs.total == nil { + return false + } else if lhs.completed == 0 && rhs.completed == 0 { + return true + } else if lhs.completed == lhs.total && rhs.completed == rhs.total { + // Both finished (1) + return true + } else if (lhs.completed == 0 && rhs.completed != 0) || (lhs.completed != 0 && rhs.completed == 0) { + // One 0, one not 0 + return false + } else { + // Cross-multiply + guard let lhsTotal = lhs.total, let rhsTotal = rhs.total else { + return false + } + + let left = lhs.completed.multipliedReportingOverflow(by: rhsTotal) + let right = lhsTotal.multipliedReportingOverflow(by: rhs.completed) + + if !left.overflow && !right.overflow { + if left.0 == right.0 { + return true + } + } else { + // Try simplifying then cross multiply again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + guard let lhsSimplified = lhsSimplified, + let rhsSimplified = rhsSimplified, + let lhsSimplifiedTotal = lhsSimplified.total, + let rhsSimplifiedTotal = rhsSimplified.total else { + // Simplification failed, fall back to doubles + return lhs.fractionCompleted == rhs.fractionCompleted + } + + let leftSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplifiedTotal) + let rightSimplified = lhsSimplifiedTotal.multipliedReportingOverflow(by: rhsSimplified.completed) + + if !leftSimplified.overflow && !rightSimplified.overflow { + if leftSimplified.0 == rightSimplified.0 { + return true + } + } else { + // Ok... fallback to doubles. This doesn't use an epsilon + return lhs.fractionCompleted == rhs.fractionCompleted + } + } + } + + return false + } + + // ---- + + internal var isFinished: Bool { + guard let total else { + return false + } + return completed >= total && completed > 0 && total > 0 + } + + internal var isIndeterminate: Bool { + return total == nil + } + + + internal var fractionCompleted : Double { + guard let total else { + return 0.0 + } + return Double(completed) / Double(total) + } + + + internal var isNaN : Bool { + return total == 0 + } + + internal var debugDescription : String { + return "\(completed) / \(total) (\(fractionCompleted)), overflowed: \(overflowed)" + } + + // ---- + + private static func _fromDouble(_ d : Double) -> (Int, Int) { + // This simplistic algorithm could someday be replaced with something better. + // Basically - how many 1/Nths is this double? + var denominator: Int + switch Int.bitWidth { + case 32: denominator = 1048576 // 2^20 - safe for 32-bit + case 64: denominator = 1073741824 // 2^30 - high precision for 64-bit + default: denominator = 131072 // 2^17 - ultra-safe fallback + } + let numerator = Int(d / (1.0 / Double(denominator))) + return (numerator, denominator) + } + + private static func _greatestCommonDivisor(_ inA : Int, _ inB : Int) -> Int { + // This is Euclid's algorithm. There are faster ones, like Knuth, but this is the simplest one for now. + var a = inA + var b = inB + repeat { + let tmp = b + b = a % b + a = tmp + } while (b != 0) + return a + } + + private static func _leastCommonMultiple(_ a : Int, _ b : Int) -> Int? { + // This division always results in an integer value because gcd(a,b) is a divisor of a. + // lcm(a,b) == (|a|/gcd(a,b))*b == (|b|/gcd(a,b))*a + let result = (a / _greatestCommonDivisor(a, b)).multipliedReportingOverflow(by: b) + if result.overflow { + return nil + } else { + return result.0 + } + } + + private static func _simplify(_ n : Int, _ d : Int) -> (Int, Int) { + let gcd = _greatestCommonDivisor(n, d) + return (n / gcd, d / gcd) + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift new file mode 100644 index 000000000..c9194bfb2 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -0,0 +1,325 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +internal import _ForSwiftFoundation +#if canImport(Synchronization) +internal import Synchronization +#endif + +//MARK: Progress Parent - Subprogress / ProgressReporter Child Interop +@available(FoundationPreview 6.4, *) +extension Progress { + + /// Returns a Subprogress which can be passed to any method that reports progress + /// It can be then used to create a child `ProgressManager` reporting to this `Progress` + /// + /// Delegates a portion of totalUnitCount to a future child `ProgressManager` instance. + /// + /// - Parameter count: Number of units delegated to a child instance of `ProgressManager` + /// which may be instantiated by `Subprogress` later when `reporter(totalCount:)` is called. + /// - Returns: A `Subprogress` instance. + public func subprogress(assigningCount count: Int) -> Subprogress { + + // Make a ProgressManager + let manager = ProgressManager(totalCount: 1) + + // Create a NSProgress - ProgressManager bridge for mirroring + let subprogressBridge = SubprogressBridge( + parent: self, + portion: Int64(count), + manager: manager + ) + + // Instantiate a Subprogress with ProgressManager as parent + // Store bridge + let subprogress = Subprogress( + parent: manager, + assignedCount: 1, + subprogressBridge: subprogressBridge + ) + + return subprogress + } + + /// Adds a ProgressReporter as a child to a Progress, which constitutes a portion of Progress's totalUnitCount. + /// + /// - Parameters: + /// - reporter: A `ProgressReporter` instance. + /// - count: Number of units delegated from `self`'s `totalCount`. + public func addChild(_ reporter: ProgressReporter, withPendingUnitCount count: Int) { + + precondition(self.isCycle(reporter: reporter) == false, "Creating a cycle is not allowed.") + + // Create a NSProgress - ProgressReporter bridge + let reporterBridge = ProgressReporterBridge( + parent: self, + portion: Int64(count), + reporterBridge: reporter + ) + + // Store bridge + reporter.manager.addBridge(reporterBridge: reporterBridge) + } + + // MARK: Cycle detection + private func isCycle(reporter: ProgressReporter, visited: Set = []) -> Bool { + guard let parent = self._parent() else { + return false + } + + guard parent is NSProgressBridge else { + return parent.isCycle(reporter: reporter) + } + + guard let unwrappedParent = (parent as? NSProgressBridge)?.manager else { + return false + } + + if unwrappedParent === reporter.manager { + return true + } + + let updatedVisited = visited.union([unwrappedParent]) + return unwrappedParent.isCycleInterop(reporter: reporter, visited: updatedVisited) + } +} + +@available(FoundationPreview 6.4, *) +//MARK: ProgressManager Parent - Progress Child Interop +extension ProgressManager { + + /// Adds a Foundation's `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. + /// - Parameters: + /// - count: Number of units delegated from `self`'s `totalCount`. + /// - progress: `Progress` which receives the delegated `count`. + public func assign(count: Int, to progress: Foundation.Progress) { + precondition(progress._parent() == nil, "Cannot assign a progress to more than one parent.") + + // Create a ProgressManager - NSProgress bridge + let progressBridge = NSProgressBridge( + manager: self, + progress: progress, + assignedCount: count + ) + + // Add bridge as a parent + progress._setParent(progressBridge, portion: Int64(count)) + + // Store bridge + self.addBridge(nsProgressBridge: progressBridge) + } +} + +@available(FoundationPreview 6.4, *) +internal final class SubprogressBridge: Sendable { + + internal let progressBridge: Progress + internal let manager: ProgressManager + + init(parent: Progress, portion: Int64, manager: ProgressManager) { + self.progressBridge = Progress(totalUnitCount: 1, parent: parent, pendingUnitCount: portion) + self.manager = manager + + manager.addObserver { [weak self] observerState in + guard let self else { + return + } + + // This needs to change totalUnitCount before completedUnitCount otherwise progressBridge will finish and mess up the math + self.progressBridge.totalUnitCount = Int64(observerState.totalCount) + self.progressBridge.completedUnitCount = Int64(observerState.completedCount) + } + } +} + +@available(FoundationPreview 6.4, *) +internal final class ProgressReporterBridge: Sendable { + + internal let progressBridge: Progress + internal let reporterBridge: ProgressReporter + + init(parent: Progress, portion: Int64, reporterBridge: ProgressReporter) { + self.progressBridge = Progress( + totalUnitCount: Int64(reporterBridge.manager.totalCount ?? 0), + parent: parent, + pendingUnitCount: portion + ) + self.progressBridge.completedUnitCount = Int64(reporterBridge.manager.completedCount) + self.reporterBridge = reporterBridge + + let manager = reporterBridge.manager + + manager.addObserver { [weak self] observerState in + guard let self else { + return + } + + self.progressBridge.totalUnitCount = Int64(observerState.totalCount) + self.progressBridge.completedUnitCount = Int64(observerState.completedCount) + } + } + +} + +@available(FoundationPreview 6.4, *) +internal final class NSProgressBridge: Progress, @unchecked Sendable { + + internal let manager: ProgressManager + internal let managerBridge: ProgressManager + internal let progress: Progress + + init(manager: ProgressManager, progress: Progress, assignedCount: Int) { + self.manager = manager + self.managerBridge = ProgressManager(totalCount: Int(clamping: progress.totalUnitCount)) + self.progress = progress + super.init(parent: nil, userInfo: nil) + + managerBridge.setCounts { completed, total in + completed = Int(clamping: progress.completedUnitCount) + } + + let position = manager.addChild( + childManager: managerBridge, + assignedCount: assignedCount, + childFraction: ProgressFraction(completed: Int(clamping: completedUnitCount), total: Int(clamping: totalUnitCount)) + ) + managerBridge.addParent(parentManager: manager, positionInParent: position) + } + + // Overrides the _updateChild func that Foundation.Progress calls to update parent + // so that the parent that gets updated is the ProgressManager parent + override func _updateChild(_ child: Foundation.Progress, fraction: _NSProgressFractionTuple, portion: Int64) { + managerBridge.setCounts { completed, total in + completed = Int(clamping: fraction.next.completed) + total = Int(clamping: fraction.next.total) + } + managerBridge.markSelfDirty() + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + // Keeping this as an enum in case we have other states to track in the future. + internal struct ObserverState { + var totalCount: Int + var completedCount: Int + } + + internal struct InteropObservation { + let subprogressBridge: SubprogressBridge? + var reporterBridge: ProgressReporterBridge? + var nsProgressBridge: Foundation.Progress? + } + + internal enum InteropType { + case interopMirror(ProgressManager) + case interopObservation(InteropObservation) + + internal var totalCount: Int? { + switch self { + case .interopMirror(let mirror): + mirror.totalCount + case .interopObservation: + nil + } + } + + internal var completedCount: Int? { + switch self { + case .interopMirror(let mirror): + mirror.completedCount + case .interopObservation: + nil + } + } + + internal var fractionCompleted: Double? { + switch self { + case .interopMirror(let mirror): + mirror.fractionCompleted + case .interopObservation: + nil + } + } + + internal var isIndeterminate: Bool? { + switch self { + case .interopMirror(let mirror): + mirror.isIndeterminate + case .interopObservation: + nil + } + } + + internal var isFinished: Bool? { + switch self { + case .interopMirror(let mirror): + mirror.isFinished + case .interopObservation: + nil + } + } + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager.State { + internal func notifyObservers(with observerState: ProgressManager.ObserverState) { + for observer in observers { + observer(observerState) + } + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + //MARK: Interop Methods + /// Adds `observer` to list of `_observers` in `self`. + internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { + state.withLock { state in + state.observers.append(observer) + } + } + + /// Notifies all `_observers` of `self` when `state` changes. + internal func notifyObservers(with observedState: ObserverState) { + state.withLock { state in + for observer in state.observers { + observer(observedState) + } + } + } + + internal func addBridge(reporterBridge: ProgressReporterBridge? = nil, nsProgressBridge: Foundation.Progress? = nil) { + state.withLock { state in + var interopObservation = InteropObservation(subprogressBridge: nil) + + if let reporterBridge { + interopObservation.reporterBridge = reporterBridge + } + + if let nsProgressBridge { + interopObservation.nsProgressBridge = nsProgressBridge + } + + state.interopType = .interopObservation(interopObservation) + } + } + + internal func setInteropChild(interopMirror: ProgressManager) { + state.withLock { state in + state.interopType = .interopMirror(interopMirror) + } + } +} +#endif diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift new file mode 100644 index 000000000..d673464b4 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -0,0 +1,538 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Synchronization) +internal import Synchronization +#endif + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + + // MARK: Methods to Read & Write Custom Properties of single ProgressManager node + /// Gets or sets custom integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Int`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom integer property type. + public subscript(dynamicMember key: KeyPath) -> Int where P.Value == Int, P.Summary == Int { + get { + if P.self == ProgressManager.Properties.TotalFileCount.self { + self.access(keyPath: \.totalFileCount) + } else if P.self == ProgressManager.Properties.CompletedFileCount.self { + self.access(keyPath: \.completedFileCount) + } else { + self.access(keyPath: \.customPropertiesInt) + } + return state.withLock { state in + if P.self == ProgressManager.Properties.TotalFileCount.self { + return state.totalFileCount + } else if P.self == ProgressManager.Properties.CompletedFileCount.self { + return state.completedFileCount + } else { + return state.customPropertiesInt[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + } + + set { + var parents: [Parent]? + if P.self == ProgressManager.Properties.TotalFileCount.self { + self.withMutation(keyPath: \.totalFileCount) { + parents = state.withLock { state in + guard newValue != state.totalFileCount else { + return nil + } + state.totalFileCount = newValue + return state.parents + } + } + } else if P.self == ProgressManager.Properties.CompletedFileCount.self { + self.withMutation(keyPath: \.completedFileCount) { + parents = state.withLock { state in + guard newValue != state.completedFileCount else { + return nil + } + state.completedFileCount = newValue + return state.parents + } + } + } else { + self.withMutation(keyPath: \.customPropertiesInt) { + parents = state.withLock { state in + guard newValue != state.customPropertiesInt[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesInt[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + } + + if let parents = parents { + if P.self == ProgressManager.Properties.TotalFileCount.self { + markSelfDirty(property: ProgressManager.Properties.TotalFileCount.self, parents: parents) + } else if P.self == ProgressManager.Properties.CompletedFileCount.self { + markSelfDirty(property: ProgressManager.Properties.CompletedFileCount.self, parents: parents) + } else { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `UInt64`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == UInt64 { + get { + if P.self == ProgressManager.Properties.TotalByteCount.self { + self.access(keyPath: \.totalByteCount) + } else if P.self == ProgressManager.Properties.CompletedByteCount.self { + self.access(keyPath: \.completedByteCount) + } else { + self.access(keyPath: \.customPropertiesUInt64) + } + return state.withLock { state in + if P.self == ProgressManager.Properties.TotalByteCount.self { + return state.totalByteCount + } else if P.self == ProgressManager.Properties.CompletedByteCount.self { + return state.completedByteCount + } else { + return state.customPropertiesUInt64[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + } + + set { + var parents: [Parent]? + if P.self == ProgressManager.Properties.TotalByteCount.self { + self.withMutation(keyPath: \.totalByteCount) { + parents = state.withLock { state in + guard newValue != state.totalByteCount else { + return nil + } + state.totalByteCount = newValue + return state.parents + } + } + } else if P.self == ProgressManager.Properties.CompletedByteCount.self { + self.withMutation(keyPath: \.completedByteCount) { + parents = state.withLock { state in + guard newValue != state.completedByteCount else { + return nil + } + state.completedByteCount = newValue + return state.parents + } + } + } else { + self.withMutation(keyPath: \.customPropertiesUInt64) { + parents = state.withLock { state in + guard newValue != state.customPropertiesUInt64[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesUInt64[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + } + + if let parents = parents { + if P.self == ProgressManager.Properties.TotalByteCount.self { + markSelfDirty(property: ProgressManager.Properties.TotalByteCount.self, parents: parents) + } else if P.self == ProgressManager.Properties.CompletedByteCount.self { + markSelfDirty(property: ProgressManager.Properties.CompletedByteCount.self, parents: parents) + } else { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + } + + /// Gets or sets custom double properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Double`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom double property type. + public subscript(dynamicMember key: KeyPath) -> P.Value where P.Value == Double, P.Summary == Double { + get { + self.access(keyPath: \.customPropertiesDouble) + return state.withLock { state in + return state.customPropertiesDouble[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + + set { + var parents: [Parent]? + self.withMutation(keyPath: \.customPropertiesDouble) { + parents = state.withLock { state in + guard newValue != state.customPropertiesDouble[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesDouble[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + + if let parents = parents { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + + /// Gets or sets custom string properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `String?` and the summary type is `[String?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom string property type. + public subscript(dynamicMember key: KeyPath) -> String? where P.Value == String?, P.Summary == [String?] { + get { + self.access(keyPath: \.customPropertiesString) + return state.withLock { state in + return state.customPropertiesString[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + + set { + var parents: [Parent]? + self.withMutation(keyPath: \.customPropertiesString) { + parents = state.withLock { state in + guard newValue != state.customPropertiesString[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesString[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + + if let parents = parents { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + + /// Gets or sets custom URL properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `URL?` and the summary type is `[URL?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom URL property type. + public subscript(dynamicMember key: KeyPath) -> URL? where P.Value == URL?, P.Summary == [URL?] { + get { + self.access(keyPath: \.customPropertiesURL) + return state.withLock { state in + return state.customPropertiesURL[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + + set { + var parents: [Parent]? + self.withMutation(keyPath: \.customPropertiesURL) { + parents = state.withLock { state in + guard newValue != state.customPropertiesURL[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesURL[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + + if let parents = parents { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `UInt64` and the summary type is `[UInt64]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == [UInt64] { + get { + if P.self == ProgressManager.Properties.Throughput.self { + self.access(keyPath: \.throughput) + } else { + self.access(keyPath: \.customPropertiesUInt64Array) + } + return state.withLock { state in + if P.self == ProgressManager.Properties.Throughput.self { + return state.throughput + } else { + return state.customPropertiesUInt64Array[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + } + + set { + var parents: [Parent]? + if P.self == ProgressManager.Properties.Throughput.self { + self.withMutation(keyPath: \.throughput) { + parents = state.withLock { state in + guard newValue != state.throughput else { + return nil + } + state.throughput = newValue + return state.parents + } + } + } else { + self.withMutation(keyPath: \.customPropertiesUInt64Array) { + parents = state.withLock { state in + guard newValue != state.customPropertiesUInt64Array[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesUInt64Array[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + } + + if let parents = parents { + if P.self == ProgressManager.Properties.Throughput.self { + markSelfDirty(property: ProgressManager.Properties.Throughput.self, parents: parents) + } else { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + } + + /// Gets or sets custom duration properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `Duration` and the summary type is `Duration`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom duration property type. + public subscript(dynamicMember key: KeyPath) -> Duration where P.Value == Duration, P.Summary == Duration { + get { + if P.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + self.access(keyPath: \.estimatedTimeRemaining) + } else { + self.access(keyPath: \.customPropertiesDuration) + } + return state.withLock { state in + if P.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + return state.estimatedTimeRemaining + } else { + return state.customPropertiesDuration[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + } + + set { + var parents: [Parent]? + if P.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + self.withMutation(keyPath: \.estimatedTimeRemaining) { + parents = state.withLock { state in + guard newValue != state.estimatedTimeRemaining else { + return nil + } + state.estimatedTimeRemaining = newValue + return state.parents + } + } + } else { + self.withMutation(keyPath: \.customPropertiesDuration) { + parents = state.withLock { state in + guard newValue != state.customPropertiesDuration[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesDuration[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + } + + if let parents = parents { + if P.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + markSelfDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.self, parents: parents) + } else { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + } + + // MARK: Methods to Read Custom Properties of Subtree with ProgressManager as root + + + /// Returns a summary for a custom integer property across the progress subtree. + /// + /// This method aggregates the values of a custom integer property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the integer property to summarize. Must be a property + /// where both the value and summary types are `Int`. + /// - Returns: An `Int` summary value for the specified property. + public func summary(of property: KeyPath) -> P.Summary where P.Value == Int, P.Summary == Int { + if P.self == ProgressManager.Properties.TotalFileCount.self { + self.access(keyPath: \.totalFileCount) + self.access(keyPath: \.totalFileCountSummary) + let updatedFileCount = updateFileCount(type: .total) + self.didSet(keyPath: \.totalFileCountSummary) + return updatedFileCount + } else if P.self == ProgressManager.Properties.CompletedFileCount.self { + self.access(keyPath: \.completedFileCount) + self.access(keyPath: \.completedFileCountSummary) + let updatedFileCount = updateFileCount(type: .completed) + self.didSet(keyPath: \.completedFileCountSummary) + return updatedFileCount + } else { + self.access(keyPath: \.customPropertiesInt) + self.access(keyPath: \.customPropertiesIntSummary) + let updatedResult = updateIntSummary(property: MetatypeWrapper(P.self)) + self.didSet(keyPath: \.customPropertiesIntSummary) + return updatedResult + } + } + + /// Returns a summary for a custom unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the unsigned integer property to summarize. Must be a property + /// where both the value and summary types are `UInt64`. + /// - Returns: An `UInt64` summary value for the specified property. + public func summary(of property: KeyPath) -> P.Summary where P.Value == UInt64, P.Summary == UInt64 { + if P.self == ProgressManager.Properties.TotalByteCount.self { + self.access(keyPath: \.totalByteCount) + self.access(keyPath: \.totalByteCountSummary) + let updatedByteCount = updateByteCount(type: .total) + self.didSet(keyPath: \.totalByteCountSummary) + return updatedByteCount + } else if P.self == ProgressManager.Properties.CompletedByteCount.self { + self.access(keyPath: \.completedByteCount) + self.access(keyPath: \.completedByteCountSummary) + let updatedByteCount = updateByteCount(type: .completed) + self.didSet(keyPath: \.completedByteCountSummary) + return updatedByteCount + } else { + self.access(keyPath: \.customPropertiesUInt64) + self.access(keyPath: \.customPropertiesUInt64Summary) + let updatedResult = updateUInt64Summary(property: MetatypeWrapper(P.self)) + self.didSet(keyPath: \.customPropertiesUInt64Summary) + return updatedResult + } + } + + /// Returns a summary for a custom double property across the progress subtree. + /// + /// This method aggregates the values of a custom double property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the double property to summarize. Must be a property + /// where both the value and summary types are `Double`. + /// - Returns: A `Double` summary value for the specified property. + public func summary(of property: KeyPath) -> P.Summary where P.Value == Double, P.Summary == Double { + self.access(keyPath: \.customPropertiesDouble) + self.access(keyPath: \.customPropertiesDoubleSummary) + let updatedResult = updateDoubleSummary(property: MetatypeWrapper(P.self)) + self.didSet(keyPath: \.customPropertiesDoubleSummary) + return updatedResult + } + + /// Returns a summary for a custom string property across the progress subtree. + /// + /// This method aggregates the values of a custom string property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the string property to summarize. Must be a property + /// where both the value type is `String?` and the summary type is `[String?]`. + /// - Returns: A `[String?]` summary value for the specified property. + public func summary(of property: KeyPath) -> P.Summary where P.Value == String?, P.Summary == [String?] { + self.access(keyPath: \.customPropertiesString) + self.access(keyPath: \.customPropertiesStringSummary) + let updatedResult = updateStringSummary(property: MetatypeWrapper(P.self)) + self.didSet(keyPath: \.customPropertiesStringSummary) + return updatedResult + } + + /// Returns a summary for a custom URL property across the progress subtree. + /// + /// This method aggregates the values of a custom URL property from this progress manager + /// and all its children, returning a consolidated summary value as an array of URLs. + /// + /// - Parameter property: The type of the URL property to summarize. Must be a property + /// where the value type is `URL?` and the summary type is `[URL?]`. + /// - Returns: A `[URL?]` summary value for the specified property. + public func summary(of property: KeyPath) -> P.Summary where P.Value == URL?, P.Summary == [URL?] { + self.access(keyPath: \.customPropertiesURL) + self.access(keyPath: \.customPropertiesURLSummary) + let updatedResult = updateURLSummary(property: MetatypeWrapper(P.self)) + self.didSet(keyPath: \.customPropertiesURLSummary) + return updatedResult + } + + /// Returns a summary for a custom unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from this progress manager + /// and all its children, returning a consolidated summary value as an array of UInt64 values. + /// + /// - Parameter property: The type of the unsigned integer property to summarize. Must be a property + /// where the value type is `UInt64` and the summary type is `[UInt64]`. + /// - Returns: A `[UInt64]` summary value for the specified property. + public func summary(of property: KeyPath) -> P.Summary where P.Value == UInt64, P.Summary == [UInt64] { + if P.self == ProgressManager.Properties.Throughput.self { + self.access(keyPath: \.throughput) + self.access(keyPath: \.throughputSummary) + let updatedThroughput = updateThroughput() + self.didSet(keyPath: \.throughputSummary) + return updatedThroughput + } else { + self.access(keyPath: \.customPropertiesUInt64Array) + self.access(keyPath: \.customPropertiesUInt64ArraySummary) + let updatedResult = updateUInt64ArraySummary(property: MetatypeWrapper(P.self)) + self.didSet(keyPath: \.customPropertiesUInt64ArraySummary) + return updatedResult + } + } + + /// Returns a summary for a custom duration property across the progress subtree. + /// + /// This method aggregates the values of a custom duration property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the duration property to summarize. Must be a property + /// where the value type is `Duration` and the summary type is `Duration`. + /// - Returns: A `Duration` summary value for the specified property. + public func summary(of property: KeyPath) -> P.Summary where P.Value == Duration, P.Summary == Duration { + if P.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + self.access(keyPath: \.estimatedTimeRemaining) + self.access(keyPath: \.estimatedTimeRemainingSummary) + let updatedTimeRemaining = updateEstimatedTimeRemaining() + self.didSet(keyPath: \.estimatedTimeRemainingSummary) + return updatedTimeRemaining + } else { + self.access(keyPath: \.customPropertiesDuration) + self.access(keyPath: \.customPropertiesDurationSummary) + let updatedResult = updateDurationSummary(property: MetatypeWrapper(P.self)) + self.didSet(keyPath: \.customPropertiesDurationSummary) + return updatedResult + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift new file mode 100644 index 000000000..cbdb45854 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift @@ -0,0 +1,277 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + + /// A type that conveys additional task-specific information on progress. + /// + /// The `Property` protocol defines custom properties that can be associated with progress tracking. + /// These properties allow you to store and aggregate additional information alongside the + /// standard progress metrics such as `totalCount` and `completedCount`. + public protocol Property: SendableMetatype { + + /// The type used for individual values of this property. + /// + /// This associated type represents the type of property values + /// that can be set on progress managers. Must be `Sendable` and `Equatable`. + /// The currently allowed types are `Int`, `Double`, `String?`, `URL?` or `UInt64`. + associatedtype Value: Sendable, Equatable + + /// The type used for aggregated summaries of this property. + /// + /// This associated type represents the type used when summarizing property values + /// across multiple progress managers in a subtree. + /// The currently allowed types are `Int`, `Double`, `[String?]`, `[URL?]` or `[UInt64]`. + associatedtype Summary: Sendable, Equatable + + /// A unique identifier for this property type. + /// + /// The key should use reverse DNS style notation to ensure uniqueness across different + /// frameworks and applications. + /// + /// - Returns: A unique string identifier for this property type. + static var key: String { get } + + /// The default value to return when property is not set to a specific value. + /// + /// This value is used when a progress manager doesn't have an explicit value set + /// for this property type. + /// + /// - Returns: The default value for this property type. + static var defaultValue: Value { get } + + /// The default summary value for this property type. + /// + /// This value is used as the initial summary when no property values have been + /// aggregated yet. + /// + /// - Returns: The default summary value for this property type. + static var defaultSummary: Summary { get } + + /// Reduces a property value into an accumulating summary. + /// + /// This method is called to incorporate individual property values into a summary + /// that represents the aggregated state across multiple progress managers. + /// + /// - Parameters: + /// - summary: The accumulating summary value to modify. + /// - value: The individual property value to incorporate into the summary. + static func reduce(into summary: inout Summary, value: Value) + + /// Merges two summary values into a single combined summary. + /// + /// This method is called to combine summary values from different branches + /// of the progress manager hierarchy into a unified summary. + /// + /// - Parameters: + /// - summary1: The first summary to merge. + /// - summary2: The second summary to merge. + /// - Returns: A new summary that represents the combination of both input summaries. + static func merge(_ summary1: Summary, _ summary2: Summary) -> Summary + + /// Determines how to handle summary data when a progress manager is deinitialized. + /// + /// This method is used when a progress manager in the hierarchy is being + /// deinitialized and its accumulated summary needs to be processed in relation to + /// its parent's summary. The behavior can vary depending on the property type: + /// + /// - For additive properties (like file counts, byte counts): The self summary + /// is typically added to the parent summary to preserve the accumulated progress. + /// - For max-based properties (like estimated time remaining): The parent summary + /// is typically preserved as it represents an existing estimate. + /// - For collection-based properties (like file URLs): The self summary may be + /// discarded to avoid accumulating stale references. + /// + /// - Parameters: + /// - parentSummary: The current summary value of the parent progress manager. + /// - selfSummary: The final summary value from the progress manager being deinitialized. + /// - Returns: The updated summary that replaces the parent's current summary. + static func finalSummary(_ parentSummary: Summary, _ selfSummary: Summary) -> Summary + } + + // Namespace for properties specific to operations reported on + @frozen + public enum Properties: Sendable { + + /// The total number of files. + public var totalFileCount: TotalFileCount.Type { TotalFileCount.self } + @frozen + public enum TotalFileCount: Sendable, Property { + + public typealias Value = Int + + public typealias Summary = Int + + public static var key: String { return "Foundation.ProgressManager.Properties.TotalFileCount" } + + public static var defaultValue: Int { return 0 } + + public static var defaultSummary: Int { return 0 } + + public static func reduce(into summary: inout Int, value: Int) { + summary += value + } + + public static func merge(_ summary1: Int, _ summary2: Int) -> Int { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: Int, _ selfSummary: Int) -> Int { + return parentSummary + selfSummary + } + } + + /// The number of completed files. + public var completedFileCount: CompletedFileCount.Type { CompletedFileCount.self } + @frozen + public enum CompletedFileCount: Sendable, Property { + + public typealias Value = Int + + public typealias Summary = Int + + public static var key: String { return "Foundation.ProgressManager.Properties.CompletedFileCount" } + + public static var defaultValue: Int { return 0 } + + public static var defaultSummary: Int { return 0 } + + public static func reduce(into summary: inout Int, value: Int) { + summary += value + } + + public static func merge(_ summary1: Int, _ summary2: Int) -> Int { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: Int, _ selfSummary: Int) -> Int { + return parentSummary + selfSummary + } + } + + /// The total number of bytes. + public var totalByteCount: TotalByteCount.Type { TotalByteCount.self } + @frozen + public enum TotalByteCount: Sendable, Property { + + public typealias Value = UInt64 + + public typealias Summary = UInt64 + + public static var key: String { return "Foundation.ProgressManager.Properties.TotalByteCount" } + + public static var defaultValue: UInt64 { return 0 } + + public static var defaultSummary: UInt64 { return 0 } + + public static func reduce(into summary: inout UInt64, value: UInt64) { + summary += value + } + + public static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: UInt64, _ selfSummary: UInt64) -> UInt64 { + return parentSummary + selfSummary + } + } + + /// The number of completed bytes. + public var completedByteCount: CompletedByteCount.Type { CompletedByteCount.self } + @frozen + public enum CompletedByteCount: Sendable, Property { + + public typealias Value = UInt64 + + public typealias Summary = UInt64 + + public static var key: String { return "Foundation.ProgressManager.Properties.CompletedByteCount" } + + public static var defaultValue: UInt64 { return 0 } + + public static var defaultSummary: UInt64 { return 0 } + + public static func reduce(into summary: inout UInt64, value: UInt64) { + summary += value + } + + public static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: UInt64, _ selfSummary: UInt64) -> UInt64 { + return parentSummary + selfSummary + } + } + + /// The throughput, in bytes per second. + public var throughput: Throughput.Type { Throughput.self } + @frozen + public enum Throughput: Sendable, Property { + public typealias Value = UInt64 + + public typealias Summary = [UInt64] + + public static var key: String { return "Foundation.ProgressManager.Properties.Throughput" } + + public static var defaultValue: UInt64 { return 0 } + + public static var defaultSummary: [UInt64] { return [] } + + public static func reduce(into summary: inout [UInt64], value: UInt64) { + summary.append(value) + } + + public static func merge(_ summary1: [UInt64], _ summary2: [UInt64]) -> [UInt64] { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: [UInt64], _ selfSummary: [UInt64]) -> [UInt64] { + return parentSummary + selfSummary + } + } + + /// The amount of time remaining in the processing of files. + public var estimatedTimeRemaining: EstimatedTimeRemaining.Type { EstimatedTimeRemaining.self } + @frozen + public enum EstimatedTimeRemaining: Sendable, Property { + + public typealias Value = Duration + + public typealias Summary = Duration + + public static var key: String { return "Foundation.ProgressManager.Properties.EstimatedTimeRemaining" } + + public static var defaultValue: Duration { return Duration.seconds(0) } + + public static var defaultSummary: Duration { return Duration.seconds(0) } + + public static func reduce(into summary: inout Duration, value: Duration) { + if summary >= value { + return + } else { + summary = value + } + } + + public static func merge(_ summary1: Duration, _ summary2: Duration) -> Duration { + return max(summary1, summary2) + } + + public static func finalSummary(_ parentSummary: Duration, _ selfSummary: Duration) -> Duration { + return parentSummary + } + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift new file mode 100644 index 000000000..c3760a667 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -0,0 +1,436 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Synchronization) +internal import Synchronization +#endif + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + + internal enum CountType { + case total + case completed + } + + //MARK: Helper Methods for Updating Dirty Path + internal func updateIntSummary(property: MetatypeWrapper) -> Int { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getIntSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.IntSummaryUpdate(index: index, updatedSummary: child.updateIntSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateIntSummary(updateInfo, updatedSummaries) + } + } + + internal func updateUInt64Summary(property: MetatypeWrapper) -> UInt64 { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getUInt64SummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.UInt64SummaryUpdate(index: index, updatedSummary: child.updateUInt64Summary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateUInt64Summary(updateInfo, updatedSummaries) + } + } + + internal func updateDoubleSummary(property: MetatypeWrapper) -> Double { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getDoubleSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.DoubleSummaryUpdate(index: index, updatedSummary: child.updateDoubleSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateDoubleSummary(updateInfo, updatedSummaries) + } + } + + internal func updateStringSummary(property: MetatypeWrapper) -> [String?] { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getStringSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.StringSummaryUpdate(index: index, updatedSummary: child.updateStringSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateStringSummary(updateInfo, updatedSummaries) + } + } + + internal func updateURLSummary(property: MetatypeWrapper) -> [URL?] { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getURLSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.URLSummaryUpdate(index: index, updatedSummary: child.updateURLSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateURLSummary(updateInfo, updatedSummaries) + } + } + + internal func updateUInt64ArraySummary(property: MetatypeWrapper) -> [UInt64] { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getUInt64ArraySummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.UInt64ArraySummaryUpdate(index: index, updatedSummary: child.updateUInt64ArraySummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateUInt64ArraySummary(updateInfo, updatedSummaries) + } + } + + internal func updateDurationSummary(property: MetatypeWrapper) -> Duration { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getDurationSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.DurationSummaryUpdate(index: index, updatedSummary: child.updateDurationSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateDurationSummary(updateInfo, updatedSummaries) + } + } + + internal func updateFileCount(type: CountType) -> Int { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getFileCountUpdateInfo(type: type) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.FileCountUpdate(index: index, updatedSummary: child.updateFileCount(type: type)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateFileCount(updateInfo, updatedSummaries) + } + } + + internal func updateByteCount(type: CountType) -> UInt64 { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getByteCountUpdateInfo(type: type) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.ByteCountUpdate(index: index, updatedSummary: child.updateByteCount(type: type)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateByteCount(updateInfo, updatedSummaries) + } + } + + internal func updateThroughput() -> [UInt64] { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getThroughputUpdateInfo() + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.ThroughputUpdate(index: index, updatedSummary: child.updateThroughput()) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateThroughput(updateInfo, updatedSummaries) + } + } + + internal func updateEstimatedTimeRemaining() -> Duration { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getEstimatedTimeRemainingUpdateInfo() + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.EstimatedTimeRemainingUpdate(index: index, updatedSummary: child.updateEstimatedTimeRemaining()) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateEstimatedTimeRemaining(updateInfo, updatedSummaries) + } + } + + //MARK: Helper Methods for Setting Dirty Paths + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.TotalFileCount.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.CompletedFileCount.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.TotalByteCount.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.CompletedByteCount.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.Throughput.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesIntSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesUInt64Summary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesDoubleSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesStringSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesURLSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesUInt64ArraySummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesDurationSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.TotalFileCount.Type, at position: Int) { + self.willSet(keyPath: \.totalFileCountSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.CompletedFileCount.Type, at position: Int) { + self.willSet(keyPath: \.completedFileCountSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.TotalByteCount.Type, at position: Int) { + self.willSet(keyPath: \.totalByteCountSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.CompletedByteCount.Type, at position: Int) { + self.willSet(keyPath: \.completedByteCountSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.Throughput.Type, at position: Int) { + self.willSet(keyPath: \.throughputSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.Type, at position: Int) { + self.willSet(keyPath: \.estimatedTimeRemainingSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + //MARK: Method to preserve values of properties upon deinit + internal func setChildDeclaredAdditionalProperties(at position: Int, totalFileCount: Int, completedFileCount: Int, totalByteCount: UInt64, completedByteCount: UInt64, throughput: [UInt64], estimatedTimeRemaining: Duration, propertiesInt: [MetatypeWrapper: Int], propertiesUInt64: [MetatypeWrapper: UInt64], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: [String?]], propertiesURL: [MetatypeWrapper: [URL?]], propertiesUInt64Array: [MetatypeWrapper: [UInt64]], propertiesDuration: [MetatypeWrapper: Duration]) { + state.withLock { state in + // The children's values are marked as non-dirty because these values are going to be in the leaf nodes. The dirty bit usually signals that there is a need to call helper method child.updatedSummary to iterate through this child's children to get updated values. But since after this level the child is already deinit, that means there is no need to clear dirty bits anymore. + state.children[position].totalFileCountSummary = PropertyStateInt(value: totalFileCount, isDirty: false) + state.children[position].completedFileCountSummary = PropertyStateInt(value: completedFileCount, isDirty: false) + state.children[position].totalByteCountSummary = PropertyStateUInt64(value: totalByteCount, isDirty: false) + state.children[position].completedByteCountSummary = PropertyStateUInt64(value: completedByteCount, isDirty: false) + state.children[position].throughputSummary = PropertyStateUInt64Array(value: throughput, isDirty: false) + state.children[position].estimatedTimeRemainingSummary = PropertyStateDuration(value: estimatedTimeRemaining, isDirty: false) + + for (propertyKey, propertyValue) in propertiesInt { + state.children[position].customPropertiesIntSummary[propertyKey] = PropertyStateInt(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesUInt64 { + state.children[position].customPropertiesUInt64Summary[propertyKey] = PropertyStateUInt64(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesDouble { + state.children[position].customPropertiesDoubleSummary[propertyKey] = PropertyStateDouble(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesString { + state.children[position].customPropertiesStringSummary[propertyKey] = PropertyStateString(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesURL { + state.children[position].customPropertiesURLSummary[propertyKey] = PropertyStateURL(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesUInt64Array { + state.children[position].customPropertiesUInt64ArraySummary[propertyKey] = PropertyStateUInt64Array(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesDuration { + state.children[position].customPropertiesDurationSummary[propertyKey] = PropertyStateDuration(value: propertyValue, isDirty: false) + } + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift new file mode 100644 index 000000000..da0823e0a --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -0,0 +1,1674 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Synchronization) +internal import Synchronization +#endif + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + + internal struct MetatypeWrapper: Hashable, Equatable, Sendable { + + let reduce: @Sendable (inout S, V) -> () + let merge: @Sendable (S, S) -> S + let finalSummary: @Sendable (S, S) -> S + + let defaultValue: V + let defaultSummary: S + + let key: String + + init(_ argument: P.Type) where P.Value == V, P.Summary == S { + reduce = P.reduce + merge = P.merge + finalSummary = P.finalSummary + defaultValue = P.defaultValue + defaultSummary = P.defaultSummary + key = P.key + } + + func hash(into hasher: inout Hasher) { + hasher.combine(key) + } + + static func == (lhs: ProgressManager.MetatypeWrapper, rhs: ProgressManager.MetatypeWrapper) -> Bool { + lhs.key == rhs.key + } + } + + internal struct PropertyStateInt { + var value: Int + var isDirty: Bool + } + + internal struct PropertyStateUInt64 { + var value: UInt64 + var isDirty: Bool + } + + internal struct PropertyStateUInt64Array { + var value: [UInt64] + var isDirty: Bool + } + + internal struct PropertyStateDuration { + var value: Duration + var isDirty: Bool + } + + internal struct PropertyStateDouble { + var value: Double + var isDirty: Bool + } + + internal struct PropertyStateString { + var value: [String?] + var isDirty: Bool + } + + internal struct PropertyStateURL { + var value: [URL?] + var isDirty: Bool + } + + internal struct PendingChildUpdateInfo { + let index: Int + let manager: ProgressManager + let wasFinished: Bool + let assignedCount: Int + } + + internal struct PendingChildUpdate { + let index: Int + let updatedFraction: ProgressFraction + let assignedCount: Int + } + + internal struct Child { + weak var manager: ProgressManager? + // portion of self's totalCount assigned to child + var assignedCount: Int + var fraction: ProgressFraction + var isFractionDirty: Bool + // Summaries of declared custom properties in subtree + var totalFileCountSummary: PropertyStateInt + var completedFileCountSummary: PropertyStateInt + var totalByteCountSummary: PropertyStateUInt64 + var completedByteCountSummary: PropertyStateUInt64 + var throughputSummary: PropertyStateUInt64Array + var estimatedTimeRemainingSummary: PropertyStateDuration + // Summaries of custom properties declared by developers in subtree + var customPropertiesIntSummary: [MetatypeWrapper: PropertyStateInt] + var customPropertiesUInt64Summary: [MetatypeWrapper: PropertyStateUInt64] + var customPropertiesDoubleSummary: [MetatypeWrapper: PropertyStateDouble] + var customPropertiesStringSummary: [MetatypeWrapper: PropertyStateString] + var customPropertiesURLSummary: [MetatypeWrapper: PropertyStateURL] + var customPropertiesUInt64ArraySummary: [MetatypeWrapper: PropertyStateUInt64Array] + var customPropertiesDurationSummary: [MetatypeWrapper: PropertyStateDuration] + } + + internal struct Parent { + var manager: ProgressManager + // self's position in parent's children list for array indexing + var positionInParent: Int + } + + internal struct State { + var selfFraction: ProgressFraction + var overallFraction: ProgressFraction { + // If any child has finished, the assigned count would have been added to selfFraction previously + var overallFraction = selfFraction + for child in children { + // So we only need to check child that has not finished, and include their fraction to overallFraction + if !child.fraction.isFinished { + let multiplier = ProgressFraction(completed: child.assignedCount, total: selfFraction.total) + if let additionalFraction = multiplier * child.fraction { + overallFraction = overallFraction + additionalFraction + } + } + } + return overallFraction + } + var children: [Child] + var parents: [Parent] + // Values of self's custom properties + var totalFileCount: Int + var completedFileCount: Int + var totalByteCount: UInt64 + var completedByteCount: UInt64 + var throughput: UInt64 + var estimatedTimeRemaining: Duration + // Values of self's custom additional properties + var customPropertiesInt: [MetatypeWrapper: Int] + var customPropertiesUInt64: [MetatypeWrapper: UInt64] + var customPropertiesDouble: [MetatypeWrapper: Double] + var customPropertiesString: [MetatypeWrapper: String?] + var customPropertiesURL: [MetatypeWrapper: URL?] + var customPropertiesUInt64Array: [MetatypeWrapper: UInt64] + var customPropertiesDuration: [MetatypeWrapper: Duration] +#if FOUNDATION_FRAMEWORK + var observers: [@Sendable (ObserverState) -> Void] + var interopType: InteropType? +#endif + + internal var totalCount: Int? { +#if FOUNDATION_FRAMEWORK + if let interopTotalCount = interopType?.totalCount { + return interopTotalCount + } +#endif + return selfFraction.total + } + + internal mutating func completedCountInfo() -> (Int, [PendingChildUpdateInfo]?) { +#if FOUNDATION_FRAMEWORK + if let interopCompletedCount = interopType?.completedCount { + return (interopCompletedCount, nil) + } +#endif + // Order is important, we need to first call pendingUpdates, then call the overallFraction.fractionCompleted otherwise the overallFraction.fractionCompleted won't capture the updates. + let pendingUpdates = pendingChildrenUpdates() + let completedCount = selfFraction.completed + return (completedCount, pendingUpdates) + } + + internal mutating func fractionCompletedInfo() -> (Double, [PendingChildUpdateInfo]?) { +#if FOUNDATION_FRAMEWORK + if let interopFractionCompleted = interopType?.fractionCompleted { + return (interopFractionCompleted, nil) + } +#endif + // Order is important, we need to first call pendingUpdates, then call the overallFraction.fractionCompleted otherwise the overallFraction.fractionCompleted won't capture the updates. + let pendingUpdates = pendingChildrenUpdates() + let fractionCompleted = overallFraction.fractionCompleted + return (fractionCompleted, pendingUpdates) + } + + internal var isIndeterminate: Bool { +#if FOUNDATION_FRAMEWORK + if let interopIsIndeterminate = interopType?.isIndeterminate { + return interopIsIndeterminate + } +#endif + return selfFraction.isIndeterminate + } + + internal mutating func isFinishedInfo() -> (Bool, [PendingChildUpdateInfo]?) { +#if FOUNDATION_FRAMEWORK + if let interopIsFinished = interopType?.isFinished { + return (interopIsFinished, nil) + } +#endif + // Order is important, we need to first call pendingUpdates, then call the overallFraction.fractionCompleted otherwise the overallFraction.fractionCompleted won't capture the updates. + let pendingUpdates = pendingChildrenUpdates() + let isFinished = selfFraction.isFinished + return (isFinished, pendingUpdates) + } + + internal mutating func complete(by count: Int) { + selfFraction.completed += count + +#if FOUNDATION_FRAMEWORK + switch interopType { + case .interopObservation(let observation): + observation.subprogressBridge?.manager.notifyObservers( + with: ObserverState( + totalCount: selfFraction.total ?? 0, + completedCount: selfFraction.completed + ) + ) + + if let _ = observation.reporterBridge { + notifyObservers( + with: ObserverState( + totalCount: selfFraction.total ?? 0, + completedCount: selfFraction.completed + ) + ) + } + case .interopMirror: + break + default: + break + } +#endif + } + + // MARK: Clean up dirty paths for fractional updates + // Returns information about upcoming updates to be done + internal mutating func pendingChildrenUpdates() -> [PendingChildUpdateInfo]? { + guard !children.isEmpty else { + return nil + } + + // Collect dirty children + var dirtyChildren: [(index: Int, manager: ProgressManager?, wasFinished: Bool, assignedCount: Int)] = [] + + for (idx, child) in children.enumerated() { + if child.isFractionDirty { + let wasFinished = children[idx].fraction.isFinished + let assignedCount = children[idx].assignedCount + dirtyChildren.append((index: idx, manager: child.manager, wasFinished: wasFinished, assignedCount: assignedCount)) + } + } + + guard !dirtyChildren.isEmpty else { + return nil + } + + for childInfo in dirtyChildren { + children[childInfo.index].isFractionDirty = false + } + + // Process dirty children, if child.manager is nil do not add to pending update array + var dirtyChildrenPendingUpdate: [PendingChildUpdateInfo] = [] + + for childInfo in dirtyChildren { + if let childManager = childInfo.manager { + // Add to pending update array + dirtyChildrenPendingUpdate.append(PendingChildUpdateInfo( + index: childInfo.index, + manager: childManager, + wasFinished: childInfo.wasFinished, + assignedCount: childInfo.assignedCount + )) + } else { + // Mark nil child as finished + if !childInfo.wasFinished { + children[childInfo.index].fraction.completed = children[childInfo.index].fraction.total ?? 0 + selfFraction.completed += childInfo.assignedCount + } + } + } + + // Return pending updates for processing + return dirtyChildrenPendingUpdate.isEmpty ? nil : dirtyChildrenPendingUpdate + } + + // Applies updates onto self's children array + internal mutating func updateChildrenProgressFraction(updates: [PendingChildUpdate]) { + for update in updates { + // Ensure the index is still valid + guard update.index < children.count else { continue } + + // Get the current state before actually updating + let currentWasFinished = children[update.index].fraction.isFinished + + children[update.index].fraction = update.updatedFraction + + // Only add to selfFraction if this update is transitioning from unfinished to finished + if update.updatedFraction.isFinished && !currentWasFinished { + selfFraction.completed += update.assignedCount + } + } + } + + // MARK: Mark paths dirty + internal mutating func markChildDirty(at position: Int) -> [Parent]? { + guard position >= 0 && position < children.count else { + return nil + } + guard !children[position].isFractionDirty else { + return nil + } + children[position].isFractionDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesIntSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesUInt64Summary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesDoubleSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesStringSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesURLSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesUInt64ArraySummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesDurationSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.TotalFileCount.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].totalFileCountSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.CompletedFileCount.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].completedFileCountSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.TotalByteCount.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].totalByteCountSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.CompletedByteCount.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].completedByteCountSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.Throughput.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].throughputSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].estimatedTimeRemainingSummary.isDirty = true + return parents + } + + // MARK: Clean up dirty paths + internal struct IntSummaryUpdateInfo { + let currentSummary: Int + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Int, isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct IntSummaryUpdate { + let index: Int + let updatedSummary: Int + } + + internal struct UInt64SummaryUpdateInfo { + let currentSummary: UInt64 + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: UInt64, isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct UInt64SummaryUpdate { + let index: Int + let updatedSummary: UInt64 + } + + internal struct DoubleSummaryUpdateInfo { + let currentSummary: Double + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Double, isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct DoubleSummaryUpdate { + let index: Int + let updatedSummary: Double + } + + internal struct StringSummaryUpdateInfo { + let currentSummary: [String?] + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: [String?], isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct StringSummaryUpdate { + let index: Int + let updatedSummary: [String?] + } + + internal struct URLSummaryUpdateInfo { + let currentSummary: [URL?] + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: [URL?], isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct URLSummaryUpdate { + let index: Int + let updatedSummary: [URL?] + } + + internal struct UInt64ArraySummaryUpdateInfo { + let currentSummary: [UInt64] + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: [UInt64], isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct UInt64ArraySummaryUpdate { + let index: Int + let updatedSummary: [UInt64] + } + + internal struct DurationSummaryUpdateInfo { + let currentSummary: Duration + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct DurationSummaryUpdate { + let index: Int + let updatedSummary: Duration + } + + internal struct FileCountUpdateInfo { + let currentSummary: Int + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Int, isAlive: Bool)] + let type: CountType + } + + internal struct FileCountUpdate { + let index: Int + let updatedSummary: Int + } + + internal struct ByteCountUpdateInfo { + let currentSummary: UInt64 + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: UInt64, isAlive: Bool)] + let type: CountType + } + + internal struct ByteCountUpdate { + let index: Int + let updatedSummary: UInt64 + } + + internal struct ThroughputUpdateInfo { + let currentSummary: [UInt64] + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: [UInt64], isAlive: Bool)] + } + + internal struct ThroughputUpdate { + let index: Int + let updatedSummary: [UInt64] + } + + internal struct EstimatedTimeRemainingUpdateInfo { + let currentSummary: Duration + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)] + } + + internal struct EstimatedTimeRemainingUpdate { + let index: Int + let updatedSummary: Duration + } + + internal mutating func getIntSummaryUpdateInfo(property: MetatypeWrapper) -> IntSummaryUpdateInfo { + var currentSummary: Int = property.defaultSummary + property.reduce(¤tSummary, customPropertiesInt[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return IntSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Int, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesIntSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultValue, isAlive)) + } + } + } + + return IntSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateIntSummary(_ updateInfo: IntSummaryUpdateInfo, _ childUpdates: [IntSummaryUpdate]) -> Int { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesIntSummary[updateInfo.property] = PropertyStateInt(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getUInt64SummaryUpdateInfo(property: MetatypeWrapper) -> UInt64SummaryUpdateInfo { + var currentSummary: UInt64 = property.defaultSummary + property.reduce(¤tSummary, customPropertiesUInt64[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return UInt64SummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: UInt64, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesUInt64Summary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultValue, isAlive)) + } + } + } + + return UInt64SummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateUInt64Summary(_ updateInfo: UInt64SummaryUpdateInfo, _ childUpdates: [UInt64SummaryUpdate]) -> UInt64 { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesUInt64Summary[updateInfo.property] = PropertyStateUInt64(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getDoubleSummaryUpdateInfo(property: MetatypeWrapper) -> DoubleSummaryUpdateInfo { + var currentSummary: Double = property.defaultSummary + property.reduce(¤tSummary, customPropertiesDouble[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return DoubleSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Double, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesDoubleSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultValue, isAlive)) + } + } + } + + return DoubleSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateDoubleSummary(_ updateInfo: DoubleSummaryUpdateInfo, _ childUpdates: [DoubleSummaryUpdate]) -> Double { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesDoubleSummary[updateInfo.property] = PropertyStateDouble(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getStringSummaryUpdateInfo(property: MetatypeWrapper) -> StringSummaryUpdateInfo { + var currentSummary: [String?] = property.defaultSummary + property.reduce(¤tSummary, customPropertiesString[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return StringSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: [String?], isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesStringSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultSummary, isAlive)) + } + } + } + + return StringSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateStringSummary(_ updateInfo: StringSummaryUpdateInfo, _ childUpdates: [StringSummaryUpdate]) -> [String?] { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesStringSummary[updateInfo.property] = PropertyStateString(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getURLSummaryUpdateInfo(property: MetatypeWrapper) -> URLSummaryUpdateInfo { + var currentSummary: [URL?] = property.defaultSummary + property.reduce(¤tSummary, customPropertiesURL[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return URLSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: [URL?], isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesURLSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultSummary, isAlive)) + } + } + } + + return URLSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateURLSummary(_ updateInfo: URLSummaryUpdateInfo, _ childUpdates: [URLSummaryUpdate]) -> [URL?] { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesURLSummary[updateInfo.property] = PropertyStateURL(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getUInt64ArraySummaryUpdateInfo(property: MetatypeWrapper) -> UInt64ArraySummaryUpdateInfo { + var currentSummary: [UInt64] = property.defaultSummary + property.reduce(¤tSummary, customPropertiesUInt64Array[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return UInt64ArraySummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: [UInt64], isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesUInt64ArraySummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultSummary, isAlive)) + } + } + } + + return UInt64ArraySummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateUInt64ArraySummary(_ updateInfo: UInt64ArraySummaryUpdateInfo, _ childUpdates: [UInt64ArraySummaryUpdate]) -> [UInt64] { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesUInt64ArraySummary[updateInfo.property] = PropertyStateUInt64Array(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getDurationSummaryUpdateInfo(property: MetatypeWrapper) -> DurationSummaryUpdateInfo { + var currentSummary: Duration = property.defaultSummary + property.reduce(¤tSummary, customPropertiesDuration[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return DurationSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesDurationSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultValue, isAlive)) + } + } + } + + return DurationSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateDurationSummary(_ updateInfo: DurationSummaryUpdateInfo, _ childUpdates: [DurationSummaryUpdate]) -> Duration { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesDurationSummary[updateInfo.property] = PropertyStateDuration(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getFileCountUpdateInfo(type: CountType) -> FileCountUpdateInfo { + let currentSummary: Int + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Int, isAlive: Bool)] = [] + + switch type { + case .total: + var value: Int = 0 + ProgressManager.Properties.TotalFileCount.reduce(into: &value, value: totalFileCount) + currentSummary = value + + guard !children.isEmpty else { + return FileCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + type: type + ) + } + + for (idx, child) in children.enumerated() { + if child.totalFileCountSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.totalFileCountSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.totalFileCountSummary.value, isAlive)) + } + } + + case .completed: + var value: Int = 0 + ProgressManager.Properties.CompletedFileCount.reduce(into: &value, value: completedFileCount) + currentSummary = value + + guard !children.isEmpty else { + return FileCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + type: type + ) + } + + for (idx, child) in children.enumerated() { + if child.completedFileCountSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.completedFileCountSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.completedFileCountSummary.value, isAlive)) + } + } + } + + return FileCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + type: type + ) + } + + internal mutating func updateFileCount(_ updateInfo: FileCountUpdateInfo, _ childUpdates: [FileCountUpdate]) -> Int { + var value = updateInfo.currentSummary + + switch updateInfo.type { + case .total: + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].totalFileCountSummary = PropertyStateInt(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.TotalFileCount.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.TotalFileCount.merge(value, childSummary) + } else { + value = ProgressManager.Properties.TotalFileCount.finalSummary(value, childSummary) + } + } + + case .completed: + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].completedFileCountSummary = PropertyStateInt(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.CompletedFileCount.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.CompletedFileCount.merge(value, childSummary) + } else { + value = ProgressManager.Properties.CompletedFileCount.finalSummary(value, childSummary) + } + } + } + + return value + } + + internal mutating func getByteCountUpdateInfo(type: CountType) -> ByteCountUpdateInfo { + let currentSummary: UInt64 + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: UInt64, isAlive: Bool)] = [] + + switch type { + case .total: + var value: UInt64 = 0 + ProgressManager.Properties.TotalByteCount.reduce(into: &value, value: totalByteCount) + currentSummary = value + + guard !children.isEmpty else { + return ByteCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + type: type + ) + } + + for (idx, child) in children.enumerated() { + if child.totalByteCountSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.totalByteCountSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.totalByteCountSummary.value, isAlive)) + } + } + + case .completed: + var value: UInt64 = 0 + ProgressManager.Properties.CompletedByteCount.reduce(into: &value, value: completedByteCount) + currentSummary = value + + guard !children.isEmpty else { + return ByteCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + type: type + ) + } + + for (idx, child) in children.enumerated() { + if child.completedByteCountSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.completedByteCountSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.completedByteCountSummary.value, isAlive)) + } + } + } + + return ByteCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + type: type + ) + } + + internal mutating func updateByteCount(_ updateInfo: ByteCountUpdateInfo, _ childUpdates: [ByteCountUpdate]) -> UInt64 { + var value = updateInfo.currentSummary + + switch updateInfo.type { + case .total: + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].totalByteCountSummary = PropertyStateUInt64(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.TotalByteCount.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.TotalByteCount.merge(value, childSummary) + } else { + value = ProgressManager.Properties.TotalByteCount.finalSummary(value, childSummary) + } + } + + case .completed: + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].completedByteCountSummary = PropertyStateUInt64(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.CompletedByteCount.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.CompletedByteCount.merge(value, childSummary) + } else { + value = ProgressManager.Properties.CompletedByteCount.finalSummary(value, childSummary) + } + } + } + + return value + } + + internal mutating func getThroughputUpdateInfo() -> ThroughputUpdateInfo { + var currentSummary = ProgressManager.Properties.Throughput.defaultSummary + ProgressManager.Properties.Throughput.reduce(into: ¤tSummary, value: throughput) + + guard !children.isEmpty else { + return ThroughputUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [] + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: [UInt64], isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if child.throughputSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.throughputSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.throughputSummary.value, isAlive)) + } + } + + return ThroughputUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries + ) + } + + internal mutating func updateThroughput(_ updateInfo: ThroughputUpdateInfo, _ childUpdates: [ThroughputUpdate]) -> [UInt64] { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].throughputSummary = PropertyStateUInt64Array(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.Throughput.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.Throughput.merge(value, childSummary) + } else { + value = ProgressManager.Properties.Throughput.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getEstimatedTimeRemainingUpdateInfo() -> EstimatedTimeRemainingUpdateInfo { + var currentSummary: Duration = Duration.seconds(0) + ProgressManager.Properties.EstimatedTimeRemaining.reduce(into: ¤tSummary, value: estimatedTimeRemaining) + + guard !children.isEmpty else { + return EstimatedTimeRemainingUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [] + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if child.estimatedTimeRemainingSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.estimatedTimeRemainingSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.estimatedTimeRemainingSummary.value, isAlive)) + } + } + + return EstimatedTimeRemainingUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries + ) + } + + internal mutating func updateEstimatedTimeRemaining(_ updateInfo: EstimatedTimeRemainingUpdateInfo, _ childUpdates: [EstimatedTimeRemainingUpdate]) -> Duration { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].estimatedTimeRemainingSummary = PropertyStateDuration(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.EstimatedTimeRemaining.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.EstimatedTimeRemaining.merge(value, childSummary) + } else { + value = ProgressManager.Properties.EstimatedTimeRemaining.finalSummary(value, childSummary) + } + } + + return value + } + + struct FinalSummary { + var totalFileCountSummary: Int + var completedFileCountSummary: Int + var totalByteCountSummary: UInt64 + var completedByteCountSummary: UInt64 + var throughputSummary: [UInt64] + var estimatedTimeRemainingSummary: Duration + var customPropertiesIntSummary: [MetatypeWrapper: Int] + var customPropertiesUInt64Summary: [MetatypeWrapper: UInt64] + var customPropertiesDoubleSummary: [MetatypeWrapper: Double] + var customPropertiesStringSummary: [MetatypeWrapper: [String?]] + var customPropertiesURLSummary: [MetatypeWrapper: [URL?]] + var customPropertiesUInt64ArraySummary: [MetatypeWrapper: [UInt64]] + var customPropertiesDurationSummary: [MetatypeWrapper: Duration] + } + + func customPropertiesCleanup() -> (FinalSummary, [Parent]) { + // Set up default summaries + var totalFileCount = Properties.TotalFileCount.defaultSummary + var completedFileCount = Properties.CompletedFileCount.defaultSummary + var totalByteCount = Properties.TotalByteCount.defaultSummary + var completedByteCount = Properties.CompletedByteCount.defaultSummary + var throughput = Properties.Throughput.defaultSummary + var estimatedTimeRemaining = Properties.EstimatedTimeRemaining.defaultSummary + var customPropertiesIntSummary: [MetatypeWrapper: Int] = [:] + var customPropertiesUInt64Summary: [MetatypeWrapper: UInt64] = [:] + var customPropertiesDoubleSummary: [MetatypeWrapper: Double] = [:] + var customPropertiesStringSummary: [MetatypeWrapper: [String?]] = [:] + var customPropertiesURLSummary: [MetatypeWrapper: [URL?]] = [:] + var customPropertiesUInt64ArraySummary: [MetatypeWrapper: [UInt64]] = [:] + var customPropertiesDurationSummary: [MetatypeWrapper: Duration] = [:] + + // Include self's custom properties values + Properties.TotalFileCount.reduce(into: &totalFileCount, value: self.totalFileCount) + Properties.CompletedFileCount.reduce(into: &completedFileCount, value: self.completedFileCount) + Properties.TotalByteCount.reduce(into: &totalByteCount, value: self.totalByteCount) + Properties.CompletedByteCount.reduce(into: &completedByteCount, value: self.completedByteCount) + Properties.Throughput.reduce(into: &throughput, value: self.throughput) + Properties.EstimatedTimeRemaining.reduce(into: &estimatedTimeRemaining, value: self.estimatedTimeRemaining) + + // MARK: Custom Properties (Int, Int) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesInt { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesIntSummary[key] = summary + } + + // MARK: Custom Properties (UInt64, UInt64) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesUInt64 { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesUInt64Summary[key] = summary + } + + // MARK: Custom Properties (UInt64, [UInt64]) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesUInt64Array { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesUInt64ArraySummary[key] = summary + } + + // MARK: Custom Properties (Double, Double) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesDouble { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesDoubleSummary[key] = summary + } + + // MARK: Custom Properties (String?, [String?]) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesString { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesStringSummary[key] = summary + } + + // MARK: Custom Properties (URL?, [URL?]) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesURL { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesURLSummary[key] = summary + } + + // MARK: Custom Properties (Duration, Duration) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesDuration { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesDurationSummary[key] = summary + } + + // Include child's custom properties summaries, we need to take into account the fact that some of the children's custom properties may not be in self, so we need to check that too. As for the ones that are in self, we need to call finalSummary. + for child in children { + totalFileCount = Properties.TotalFileCount.finalSummary(totalFileCount, child.totalFileCountSummary.value) + completedFileCount = Properties.CompletedFileCount.finalSummary(completedFileCount, child.completedFileCountSummary.value) + totalByteCount = Properties.TotalByteCount.finalSummary(totalByteCount, child.totalByteCountSummary.value) + completedByteCount = Properties.CompletedByteCount.finalSummary(completedByteCount, child.completedByteCountSummary.value) + throughput = Properties.Throughput.finalSummary(throughput, child.throughputSummary.value) + estimatedTimeRemaining = Properties.EstimatedTimeRemaining.finalSummary(estimatedTimeRemaining, child.estimatedTimeRemainingSummary.value) + + for (key, _) in customPropertiesInt { + customPropertiesIntSummary[key] = key.finalSummary(customPropertiesIntSummary[key] ?? key.defaultSummary, child.customPropertiesIntSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesIntSummary { + if !customPropertiesInt.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesIntSummary[key] = summary + } + } + + + for (key, _) in customPropertiesUInt64 { + customPropertiesUInt64Summary[key] = key.finalSummary(customPropertiesUInt64Summary[key] ?? key.defaultSummary, child.customPropertiesUInt64Summary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesUInt64Summary { + if !customPropertiesUInt64.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesUInt64Summary[key] = summary + } + } + + + for (key, _) in customPropertiesUInt64Array { + customPropertiesUInt64ArraySummary[key] = key.finalSummary(customPropertiesUInt64ArraySummary[key] ?? key.defaultSummary, child.customPropertiesUInt64ArraySummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesUInt64ArraySummary { + if !customPropertiesUInt64Array.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesUInt64ArraySummary[key] = summary + } + } + + for (key, _) in customPropertiesDouble { + customPropertiesDoubleSummary[key] = key.finalSummary(customPropertiesDoubleSummary[key] ?? key.defaultSummary, child.customPropertiesDoubleSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesDoubleSummary { + if !customPropertiesDouble.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesDoubleSummary[key] = summary + } + } + + for (key, _) in customPropertiesString { + customPropertiesStringSummary[key] = key.finalSummary(customPropertiesStringSummary[key] ?? key.defaultSummary, child.customPropertiesStringSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesStringSummary { + if !customPropertiesString.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesStringSummary[key] = summary + } + } + + for (key, _) in customPropertiesURL { + customPropertiesURLSummary[key] = key.finalSummary(customPropertiesURLSummary[key] ?? key.defaultSummary, child.customPropertiesURLSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesURLSummary { + if !customPropertiesURL.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesURLSummary[key] = summary + } + } + + for (key, _) in customPropertiesDuration { + customPropertiesDurationSummary[key] = key.finalSummary(customPropertiesDurationSummary[key] ?? key.defaultSummary, child.customPropertiesDurationSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesDurationSummary { + if !customPropertiesDuration.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesDurationSummary[key] = summary + } + } + } + + return (FinalSummary(totalFileCountSummary: totalFileCount, + completedFileCountSummary: completedFileCount, + totalByteCountSummary: totalByteCount, + completedByteCountSummary: completedByteCount, + throughputSummary: throughput, + estimatedTimeRemainingSummary: estimatedTimeRemaining, + customPropertiesIntSummary: customPropertiesIntSummary, + customPropertiesUInt64Summary: customPropertiesUInt64Summary, + customPropertiesDoubleSummary: customPropertiesDoubleSummary, + customPropertiesStringSummary: customPropertiesStringSummary, + customPropertiesURLSummary: customPropertiesURLSummary, + customPropertiesUInt64ArraySummary: customPropertiesUInt64ArraySummary, + customPropertiesDurationSummary: customPropertiesDurationSummary + ), + parents) + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift new file mode 100644 index 000000000..595c8e258 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -0,0 +1,567 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Observation +#if canImport(Synchronization) +internal import Synchronization +#endif + +#if canImport(CollectionsInternal) +internal import CollectionsInternal +#elseif canImport(OrderedCollections) +internal import OrderedCollections +#elseif canImport(_FoundationCollections) +internal import _FoundationCollections +#endif + +/// An object that conveys ongoing progress to the user for a specified task. +@available(FoundationPreview 6.4, *) +@dynamicMemberLookup +@Observable public final class ProgressManager: Sendable { + + internal let state: Mutex + // These are "fake" keypaths used for registering observations of values and summaries of custom properties declared by developers. + internal let customPropertiesInt: Void + internal let customPropertiesUInt64: Void + internal let customPropertiesDouble: Void + internal let customPropertiesString: Void + internal let customPropertiesURL: Void + internal let customPropertiesUInt64Array: Void + internal let customPropertiesDuration: Void + internal let customPropertiesIntSummary: Void + internal let customPropertiesUInt64Summary: Void + internal let customPropertiesDoubleSummary: Void + internal let customPropertiesStringSummary: Void + internal let customPropertiesURLSummary: Void + internal let customPropertiesUInt64ArraySummary: Void + internal let customPropertiesDurationSummary: Void + // These are "fake" keypaths used for registering observations of summaries of pre-declared custom properties. + internal let totalFileCountSummary: Void + internal let completedFileCountSummary: Void + internal let totalByteCountSummary: Void + internal let completedByteCountSummary: Void + internal let throughputSummary: Void + internal let estimatedTimeRemainingSummary: Void + + /// The total units of work. + public var totalCount: Int? { + self.access(keyPath: \.totalCount) + return state.withLock { state in + state.totalCount + } + } + + /// The completed units of work. + public var completedCount: Int { + self.access(keyPath: \.completedCount) + + // Get information about dirty children (Acquire and release self's lock) + let (children, completedCount, pendingUpdates) = state.withLock { state in + let (completedCount, pendingUpdates) = state.completedCountInfo() + return (state.children.compactMap { $0.manager }, completedCount, pendingUpdates) + } + + guard let updates = pendingUpdates else { + for child in children { + child.access(keyPath: \.completedCount) + } + return completedCount + } + + // Get updated information for each dirty child (Acquire and release each child's lock) + var childrenUpdates: [PendingChildUpdate] = [] + + for update in updates { + let updatedFraction = update.manager.updatedProgressFraction() + childrenUpdates.append(PendingChildUpdate( + index: update.index, + updatedFraction: updatedFraction, + assignedCount: update.assignedCount + )) + } + + // Apply updated information of dirty child in self's children array (Acquire and release self's lock) + let updatedCompletedCount = state.withLock { state in + state.updateChildrenProgressFraction(updates: childrenUpdates) + return state.selfFraction.completed + } + + for child in children { + child.access(keyPath: \.completedCount) + } + + return updatedCompletedCount + } + + /// The proportion of work completed. + /// This takes into account the fraction completed in its children instances if children are present. + /// If `self` is indeterminate, the value will be 0.0. + public var fractionCompleted: Double { + self.access(keyPath: \.totalCount) + self.access(keyPath: \.completedCount) + + // Get information about dirty children (Acquire and release self's lock) + let (children, fractionCompleted, pendingUpdates) = state.withLock { state in + let (fractionCompleted, pendingUpdates) = state.fractionCompletedInfo() + return (state.children.compactMap { $0.manager }, fractionCompleted, pendingUpdates) + } + + guard let updates = pendingUpdates else { + for child in children { + child.access(keyPath: \.totalCount) + child.access(keyPath: \.completedCount) + } + return fractionCompleted + } + + // Get updated information for each dirty child (Acquire and release each child's lock) + var childrenUpdates: [PendingChildUpdate] = [] + + for update in updates { + let updatedFraction = update.manager.updatedProgressFraction() + childrenUpdates.append(PendingChildUpdate( + index: update.index, + updatedFraction: updatedFraction, + assignedCount: update.assignedCount + )) + } + + // Apply updated information of dirty child in self's children array (Acquire and release self's lock) + let updatedFractionCompleted = state.withLock { state in + state.updateChildrenProgressFraction(updates: childrenUpdates) + return state.overallFraction.fractionCompleted + } + + for child in children { + child.access(keyPath: \.totalCount) + child.access(keyPath: \.completedCount) + } + + return updatedFractionCompleted + } + + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. + public var isIndeterminate: Bool { + self.access(keyPath: \.totalCount) + return state.withLock { state in + state.isIndeterminate + } + } + + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. + public var isFinished: Bool { + self.access(keyPath: \.totalCount) + self.access(keyPath: \.completedCount) + + // Get information about dirty children (Acquire and release self's lock) + let (children, isFinished, pendingUpdates) = state.withLock { state in + let (isFinished, pendingUpdates) = state.isFinishedInfo() + return (state.children.compactMap { $0.manager }, isFinished, pendingUpdates) + } + + guard let updates = pendingUpdates else { + for child in children { + child.access(keyPath: \.totalCount) + child.access(keyPath: \.completedCount) + } + return isFinished + } + + // Get updated information for each dirty child (Acquire and release each child's lock) + var childrenUpdates: [PendingChildUpdate] = [] + + for update in updates { + let updatedFraction = update.manager.updatedProgressFraction() + childrenUpdates.append(PendingChildUpdate( + index: update.index, + updatedFraction: updatedFraction, + assignedCount: update.assignedCount + )) + } + + // Apply updated information of dirty child in self's children array (Acquire and release self's lock) + let updatedIsFinished = state.withLock { state in + state.updateChildrenProgressFraction(updates: childrenUpdates) + return state.selfFraction.isFinished + } + + for child in children { + child.access(keyPath: \.completedCount) + } + + return updatedIsFinished + } + + /// A `ProgressReporter` instance, used for providing read-only observation of progress updates or composing into other `ProgressManager`s. + public var reporter: ProgressReporter { + return .init(manager: self) + } + +#if FOUNDATION_FRAMEWORK + internal init(total: Int?, completed: Int?, subprogressBridge: SubprogressBridge?) { + let state = State( + selfFraction: ProgressFraction(completed: completed ?? 0, total: total), + children: [], + parents: [], + totalFileCount: ProgressManager.Properties.TotalFileCount.defaultValue, + completedFileCount: ProgressManager.Properties.CompletedFileCount.defaultValue, + totalByteCount: ProgressManager.Properties.TotalByteCount.defaultValue, + completedByteCount: ProgressManager.Properties.CompletedByteCount.defaultValue, + throughput: ProgressManager.Properties.Throughput.defaultValue, + estimatedTimeRemaining: ProgressManager.Properties.EstimatedTimeRemaining.defaultValue, + customPropertiesInt: [:], + customPropertiesUInt64: [:], + customPropertiesDouble: [:], + customPropertiesString: [:], + customPropertiesURL: [:], + customPropertiesUInt64Array: [:], + customPropertiesDuration: [:], + observers: [], + interopType: .interopObservation(InteropObservation(subprogressBridge: subprogressBridge)) + ) + self.state = Mutex(state) + } +#else + internal init(total: Int?, completed: Int?) { + let state = State( + selfFraction: ProgressFraction(completed: completed ?? 0, total: total), + children: [], + parents: [], + totalFileCount: ProgressManager.Properties.TotalFileCount.defaultValue, + completedFileCount: ProgressManager.Properties.CompletedFileCount.defaultValue, + totalByteCount: ProgressManager.Properties.TotalByteCount.defaultValue, + completedByteCount: ProgressManager.Properties.CompletedByteCount.defaultValue, + throughput: ProgressManager.Properties.Throughput.defaultValue, + estimatedTimeRemaining: ProgressManager.Properties.EstimatedTimeRemaining.defaultValue, + customPropertiesInt: [:], + customPropertiesUInt64: [:], + customPropertiesDouble: [:], + customPropertiesString: [:], + customPropertiesURL: [:], + customPropertiesUInt64Array: [:], + customPropertiesDuration: [:] + ) + self.state = Mutex(state) + } +#endif + + /// Initializes `self` with `totalCount`. + /// + /// If `totalCount` is set to `nil`, `self` is indeterminate. + /// - Parameter totalCount: Total units of work. + public convenience init(totalCount: Int?) { + #if FOUNDATION_FRAMEWORK + self.init( + total: totalCount, + completed: nil, + subprogressBridge: nil + ) + #else + self.init( + total: totalCount, + completed: nil, + ) + #endif + } + + /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. + /// + /// If the `Subprogress` is not converted into a `ProgressManager` (for example, due to an error or early return), + /// then the assigned count is marked as completed in the parent `ProgressManager`. + /// + /// - Parameter count: The portion of `totalCount` to be delegated to the `Subprogress`. + /// - Returns: A `Subprogress` instance. + public func subprogress(assigningCount portionOfParentTotal: Int) -> Subprogress { + precondition(portionOfParentTotal > 0, "Giving out zero units is not a valid operation.") + let subprogress = Subprogress(parent: self, assignedCount: portionOfParentTotal) + return subprogress + } + + /// Adds a `ProgressReporter` as a child, with its progress representing a portion of `self`'s progress. + /// + /// If a cycle is detected, this will cause a crash at runtime. + /// + /// - Parameters: + /// - count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. + /// - reporter: A `ProgressReporter` instance. + public func assign(count: Int, to reporter: ProgressReporter) { + precondition(isCycle(reporter: reporter) == false, "Creating a cycle is not allowed.") + + let actualManager = reporter.manager + + let position = self.addChild(childManager: actualManager, assignedCount: count, childFraction: actualManager.getProgressFraction()) + actualManager.addParent(parentManager: self, positionInParent: position) + } + + /// Increases `completedCount` by `count`. + /// - Parameter count: Units of work. + public func complete(count: Int) { + self.withMutation(keyPath: \.completedCount) { + let parents: [Parent]? = state.withLock { state in + guard state.selfFraction.completed != (state.selfFraction.completed + count) else { + return nil + } + + state.complete(by: count) + + return state.parents + } + if let parents = parents { + markSelfDirty(parents: parents) + } + } + } + + public func setCounts(_ counts: (_ completed: inout Int, _ total: inout Int?) -> Void) { + self.withMutation(keyPath: \.completedCount) { + self.withMutation(keyPath: \.totalCount) { + let parents: [Parent]? = state.withLock { state in + var completed = state.selfFraction.completed + var total = state.selfFraction.total + + counts(&completed, &total) + + guard state.selfFraction.completed != completed || state.selfFraction.total != total else { + return nil + } + + state.selfFraction.completed = completed + state.selfFraction.total = total + + return state.parents + } + + if let parents = parents { + markSelfDirty(parents: parents) + } + } + } + } + + //MARK: Observation Methods + internal func willSet(keyPath: KeyPath) { + _$observationRegistrar.willSet(self, keyPath: keyPath) + } + + internal func didSet(keyPath: KeyPath) { + _$observationRegistrar.didSet(self, keyPath: keyPath) + } + + //MARK: Fractional Properties Methods + internal func getProgressFraction() -> ProgressFraction { + return state.withLock { state in + return state.selfFraction + } + } + + //MARK: Fractional Calculation methods + internal func markSelfDirty() { + let parents = state.withLock { state in + return state.parents + } + markSelfDirty(parents: parents) + } + + internal func markSelfDirty(parents: [Parent]) { + if parents.count > 0 { + for parent in parents { + parent.manager.markChildDirty(at: parent.positionInParent) + } + } + } + + private func markChildDirty(at position: Int) { + let parents: [Parent]? = state.withLock { state in + state.markChildDirty(at: position) + } + if let parents = parents { + markSelfDirty(parents: parents) + } + } + + internal func updatedProgressFraction() -> ProgressFraction { + // Get information about dirty children (Acquire and release self's lock) + let pendingUpdates = state.withLock { state in + state.pendingChildrenUpdates() + } + + guard let updates = pendingUpdates else { + // No pending updates, just return the overall fraction + return state.withLock { state in + state.overallFraction + } + } + + // Get updated information for each dirty child (Acquire and release each child's lock) + var childrenUpdates: [PendingChildUpdate] = [] + + for update in updates { + let updatedFraction = update.manager.updatedProgressFraction() + childrenUpdates.append(PendingChildUpdate( + index: update.index, + updatedFraction: updatedFraction, + assignedCount: update.assignedCount + )) + } + + // Apply updated information of dirty child in self's children array (Acquire and release self's lock) + return state.withLock { state in + state.updateChildrenProgressFraction(updates: childrenUpdates) + return state.overallFraction + } + } + + + //MARK: Parent - Child Relationship Methods + internal func addChild(childManager: ProgressManager, assignedCount: Int, childFraction: ProgressFraction) -> Int { + self.withMutation(keyPath: \.completedCount) { + let (index, parents) = state.withLock { state in + let child = Child(manager: childManager, + assignedCount: assignedCount, + fraction: childFraction, + isFractionDirty: true, + totalFileCountSummary: PropertyStateInt(value: ProgressManager.Properties.TotalFileCount.defaultSummary, isDirty: false), + completedFileCountSummary: PropertyStateInt(value: ProgressManager.Properties.CompletedFileCount.defaultSummary, isDirty: false), + totalByteCountSummary: PropertyStateUInt64(value: ProgressManager.Properties.TotalByteCount.defaultSummary, isDirty: false), + completedByteCountSummary: PropertyStateUInt64(value: ProgressManager.Properties.CompletedByteCount.defaultSummary, isDirty: false), + throughputSummary: PropertyStateUInt64Array(value: ProgressManager.Properties.Throughput.defaultSummary, isDirty: false), + estimatedTimeRemainingSummary: PropertyStateDuration(value: ProgressManager.Properties.EstimatedTimeRemaining.defaultSummary, isDirty: false), + customPropertiesIntSummary: [:], + customPropertiesUInt64Summary: [:], + customPropertiesDoubleSummary: [:], + customPropertiesStringSummary: [:], + customPropertiesURLSummary: [:], + customPropertiesUInt64ArraySummary: [:], + customPropertiesDurationSummary: [:]) + state.children.append(child) + return (state.children.count - 1, state.parents) + } + // Mark dirty all the way up to the root so that if the branch was marked not dirty right before this it will be marked dirty again (for optimization to work) + markSelfDirty(parents: parents) + return index + } + } + + internal func addParent(parentManager: ProgressManager, positionInParent: Int) { + state.withLock { state in + let parent = Parent(manager: parentManager, positionInParent: positionInParent) + state.parents.append(parent) + } + } + + // MARK: Cycle Detection Methods + internal func isCycle(reporter: ProgressReporter, visited: Set = []) -> Bool { + if reporter.manager === self { + return true + } + let updatedVisited = visited.union([self]) + let parents = state.withLock { state in + return state.parents + } + for parent in parents { + guard !updatedVisited.contains(parent.manager) else { + continue + } + if parent.manager.isCycle(reporter: reporter, visited: updatedVisited) { + return true + } + } + return false + } + + internal func isCycleInterop(reporter: ProgressReporter, visited: Set = []) -> Bool { + let parents = state.withLock { state in + return state.parents + } + for parent in parents { + guard !visited.contains(parent.manager) else { + continue + } + if parent.manager.isCycle(reporter: reporter, visited: visited) { + return true + } + } + return false + } + + deinit { + // Custom Properties directly updates parents' entries so it should not be marked dirty. + let (finalSummary, parents) = state.withLock { state in + state.customPropertiesCleanup() + } + + for parent in parents { + parent.manager.setChildDeclaredAdditionalProperties( + at: parent.positionInParent, + totalFileCount: finalSummary.totalFileCountSummary, + completedFileCount: finalSummary.completedFileCountSummary, + totalByteCount: finalSummary.totalByteCountSummary, + completedByteCount: finalSummary.completedByteCountSummary, + throughput: finalSummary.throughputSummary, + estimatedTimeRemaining: finalSummary.estimatedTimeRemainingSummary, + propertiesInt: finalSummary.customPropertiesIntSummary, + propertiesUInt64: finalSummary.customPropertiesUInt64Summary, + propertiesDouble: finalSummary.customPropertiesDoubleSummary, + propertiesString: finalSummary.customPropertiesStringSummary, + propertiesURL: finalSummary.customPropertiesURLSummary, + propertiesUInt64Array: finalSummary.customPropertiesUInt64ArraySummary, + propertiesDuration: finalSummary.customPropertiesDurationSummary + ) + } + + // Fractional property does not update parents' entries so it should be marked dirty. If fraction is not finished when deinit, mark the path dirty so that parents have an opportunity to check and complete the portion that is unfinished. The parent will later complete the portion when trying to clear the dirty bit in state.updateChildrenProgressFraction(). + if !isFinished { + markSelfDirty(parents: parents) + } + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager: Hashable, Equatable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + /// Returns `true` if pointer of `lhs` is equal to pointer of `rhs`. + public static func ==(lhs: ProgressManager, rhs: ProgressManager) -> Bool { + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager: CustomStringConvertible, CustomDebugStringConvertible { + /// A description. + public var description: String { + return """ + Class Name: ProgressManager + Object Identifier: \(ObjectIdentifier(self)) + totalCount: \(String(describing: totalCount)) + completedCount: \(completedCount) + fractionCompleted: \(fractionCompleted) + isIndeterminate: \(isIndeterminate) + isFinished: \(isFinished) + totalFileCount: \(summary(of: \.totalFileCount)) + completedFileCount: \(summary(of: \.completedFileCount)) + totalByteCount: \(summary(of: \.totalByteCount)) + completedByteCount: \(summary(of: \.completedByteCount)) + throughput: \(summary(of: \.throughput)) + estimatedTimeRemaining: \(summary(of: \.estimatedTimeRemaining)) + """ + } + + /// A debug description. + public var debugDescription: String { + return self.description + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift new file mode 100644 index 000000000..929d75961 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -0,0 +1,273 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Observation + +/// ProgressReporter is a wrapper for ProgressManager that carries information about ProgressManager. +/// +/// It is read-only and can be added as a child of another ProgressManager. +@available(FoundationPreview 6.4, *) +@dynamicMemberLookup +@Observable public final class ProgressReporter: Sendable, Hashable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { + + public typealias Property = ProgressManager.Property + + /// The total units of work. + public var totalCount: Int? { + manager.totalCount + } + + /// The completed units of work. + /// If `self` is indeterminate, the value will be 0. + public var completedCount: Int { + manager.completedCount + } + + /// The proportion of work completed. + /// This takes into account the fraction completed in its children instances if children are present. + /// If `self` is indeterminate, the value will be 0. + public var fractionCompleted: Double { + manager.fractionCompleted + } + + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. + public var isIndeterminate: Bool { + manager.isIndeterminate + } + + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. + public var isFinished: Bool { + manager.isFinished + } + + /// A textual representation of the progress reporter. + /// + /// This property provides a comprehensive description including the class name, object identifier, + /// underlying progress manager details, and various progress metrics and properties. + public var description: String { + return """ + Class Name: ProgressReporter + Object Identifier: \(ObjectIdentifier(self)) + progressManager: \(manager) + totalCount: \(String(describing: totalCount)) + completedCount: \(completedCount) + fractionCompleted: \(fractionCompleted) + isIndeterminate: \(isIndeterminate) + isFinished: \(isFinished) + totalFileCount: \(summary(of: \.totalFileCount)) + completedFileCount: \(summary(of: \.completedFileCount)) + totalByteCount: \(summary(of: \.totalByteCount)) + completedByteCount: \(summary(of: \.completedByteCount)) + throughput: \(summary(of: \.throughput)) + estimatedTimeRemaining: \(summary(of: \.estimatedTimeRemaining)) + """ + } + + /// A textual representation of the progress reporter suitable for debugging. + /// + /// This property returns the same value as `description`, providing detailed information + /// about the progress reporter's state for debugging purposes. + public var debugDescription: String { + return self.description + } + + /// Returns a summary for the specified integer property across the progress subtree. + /// + /// This method aggregates the values of a custom integer property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the integer property to summarize. Must be a property + /// where both the value and summary types are `Int`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: KeyPath) -> Int where P.Value == Int, P.Summary == Int { + manager.summary(of: property) + } + + /// Returns a summary for the specified unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the unsigned property to summarize. Must be a property + /// where both the value and summary types are `UInt64`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == UInt64 { + manager.summary(of: property) + } + + /// Returns a summary for the specified double property across the progress subtree. + /// + /// This method aggregates the values of a custom double property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the double property to summarize. Must be a property + /// where both the value and summary types are `Double`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: KeyPath) -> Double where P.Value == Double, P.Summary == Double { + manager.summary(of: property) + } + + /// Returns a summary for the specified string property across the progress subtree. + /// + /// This method aggregates the values of a custom string property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the string property to summarize. Must be a property + /// where both the value and summary types are `String`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: KeyPath) -> [String?] where P.Value == String?, P.Summary == [String?] { + return manager.summary(of: property) + } + + /// Returns a summary for the specified URL property across the progress subtree. + /// + /// This method aggregates the values of a custom URL property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the URL property to summarize. Must be a property + /// where both the value and summary types are `URL?` and `[URL?]` respectively. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: KeyPath) -> [URL?] where P.Value == URL?, P.Summary == [URL?] { + return manager.summary(of: property) + } + + /// Returns a summary for the specified unsigned integer array property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from the underlying progress manager + /// and all its children, returning a consolidated summary value as an array. + /// + /// - Parameter property: The type of the unsigned integer property to summarize. Must be a property + /// where the value type is `UInt64` and the summary type is `[UInt64]`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: KeyPath) -> [UInt64] where P.Value == UInt64, P.Summary == [UInt64] { + return manager.summary(of: property) + } + + /// Returns a summary for the specified duration property across the progress subtree. + /// + /// This method aggregates the values of a custom duration property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the duration property to summarize. Must be a property + /// where both the value and summary types are `Duration`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: KeyPath) -> Duration where P.Value == Duration, P.Summary == Duration { + return manager.summary(of: property) + } + + /// Gets or sets custom integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Int`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom integer property type. + public subscript(dynamicMember key: KeyPath) -> Int where P.Value == Int, P.Summary == Int { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `UInt64`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == UInt64 { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom double properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Double`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom double property type. + public subscript(dynamicMember key: KeyPath) -> P.Value where P.Value == Double, P.Summary == Double { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom string properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `String?` and the summary type is `[String?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom string property type. + public subscript(dynamicMember key: KeyPath) -> String? where P.Value == String?, P.Summary == [String?] { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom URL properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `URL?` and the summary type is `[URL?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom URL property type. + public subscript(dynamicMember key: KeyPath) -> URL? where P.Value == URL?, P.Summary == [URL?] { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `UInt64` and the summary type is `[UInt64]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == [UInt64] { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom duration properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `Duration` and the summary type is `Duration`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom duration property type. + public subscript(dynamicMember key: KeyPath) -> Duration where P.Value == Duration, P.Summary == Duration { + get { + manager[dynamicMember: key] + } + } + + internal let manager: ProgressManager + + internal init(manager: ProgressManager) { + self.manager = manager + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + public static func == (lhs: ProgressReporter, rhs: ProgressReporter) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/Subprogress.swift b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift new file mode 100644 index 000000000..2e960d4bd --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Subprogress is used to establish parent-child relationship between two instances of `ProgressManager`. +/// +/// Subprogress is returned from a call to `subprogress(assigningCount:)` by a parent ProgressManager. +/// A child ProgressManager is then returned by calling `start(totalCount:)` on a Subprogress. +@available(FoundationPreview 6.4, *) +public struct Subprogress: ~Copyable, Sendable { + internal var parent: ProgressManager + internal var assignedCount: Int + internal var isInitializedToProgressReporter: Bool +#if FOUNDATION_FRAMEWORK + internal var subprogressBridge: SubprogressBridge? +#endif + +#if FOUNDATION_FRAMEWORK + internal init(parent: ProgressManager, assignedCount: Int, subprogressBridge: SubprogressBridge? = nil) { + self.parent = parent + self.assignedCount = assignedCount + self.isInitializedToProgressReporter = false + self.subprogressBridge = subprogressBridge + } +#else + internal init(parent: ProgressManager, assignedCount: Int) { + self.parent = parent + self.assignedCount = assignedCount + self.isInitializedToProgressReporter = false + } +#endif + + /// Instantiates a ProgressManager which is a child to the parent from which `self` is returned. + /// - Parameter totalCount: Total count of returned child `ProgressManager` instance. + /// - Returns: A `ProgressManager` instance. + public consuming func start(totalCount: Int?) -> ProgressManager { + isInitializedToProgressReporter = true + +#if FOUNDATION_FRAMEWORK + let childManager = ProgressManager( + total: totalCount, + completed: nil, + subprogressBridge: subprogressBridge + ) + + guard subprogressBridge == nil else { + subprogressBridge?.manager.setInteropChild(interopMirror: childManager) + return childManager + } +#else + let childManager = ProgressManager( + total: totalCount, + completed: nil + ) +#endif + + let position = parent.addChild( + childManager: childManager, + assignedCount: assignedCount, + childFraction: childManager.getProgressFraction() + ) + childManager.addParent( + parentManager: parent, + positionInParent: position + ) + + return childManager + } + + deinit { + if !self.isInitializedToProgressReporter { + parent.complete(count: assignedCount) + } + } +} diff --git a/Sources/FoundationEssentials/String/String+IO.swift b/Sources/FoundationEssentials/String/String+IO.swift index 640efe422..39cf21b7e 100644 --- a/Sources/FoundationEssentials/String/String+IO.swift +++ b/Sources/FoundationEssentials/String/String+IO.swift @@ -456,7 +456,7 @@ extension StringProtocol { let options : Data.WritingOptions = useAuxiliaryFile ? [.atomic] : [] #endif - try writeToFile(path: .path(String(path)), data: data, options: options, attributes: attributes, reportProgress: false) + try writeToFile(path: .path(String(path)), buffer: data.bytes, options: options, attributes: attributes, reportProgress: false) } /// Writes the contents of the `String` to the URL specified by url using the specified encoding. @@ -479,7 +479,7 @@ extension StringProtocol { let options : Data.WritingOptions = useAuxiliaryFile ? [.atomic] : [] #endif - try writeToFile(path: .url(url), data: data, options: options, attributes: attributes, reportProgress: false) + try writeToFile(path: .url(url), buffer: data.bytes, options: options, attributes: attributes, reportProgress: false) } #endif } diff --git a/Tests/FoundationEssentialsTests/DataIOTests.swift b/Tests/FoundationEssentialsTests/DataIOTests.swift index 4ebca2557..c2ba8c6d1 100644 --- a/Tests/FoundationEssentialsTests/DataIOTests.swift +++ b/Tests/FoundationEssentialsTests/DataIOTests.swift @@ -133,7 +133,7 @@ private final class DataIOTests { // Data doesn't have a direct API to write with attributes, but our I/O code has it. Use it via @testable interface here. let writeAttrs: [String : Data] = [FileAttributeKey.hfsCreatorCode.rawValue : "abcd".data(using: .ascii)!] - try writeToFile(path: .url(url), data: writeData, options: [], attributes: writeAttrs) + try writeToFile(path: .url(url), buffer: writeData.bytes, options: [], attributes: writeAttrs) // Verify attributes var readAttrs: [String : Data] = [:] diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift new file mode 100644 index 000000000..49728d456 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift @@ -0,0 +1,167 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +@Suite("Progress Fraction", .tags(.progressManager)) struct ProgressFractionTests { + @Test func equal() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 100, total: 200) + + #expect(f1 == f2) + + let f3 = ProgressFraction(completed: 3, total: 10) + #expect(f1 != f3) + + let f4 = ProgressFraction(completed: 5, total: 10) + #expect(f1 == f4) + } + + @Test func addSame() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 3, total: 10) + + let r = f1 + f2 + #expect(r.completed == 8) + #expect(r.total == 10) + } + + @Test func addDifferent() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed : 300, total: 1000) + + let r = f1 + f2 + #expect(r.completed == 800) + #expect(r.total == 1000) + } + + @Test func subtract() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 3, total: 10) + + let r = f1 - f2 + #expect(r.completed == 2) + #expect(r.total == 10) + } + + @Test func multiply() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 1, total: 2) + + let r = f1 * f2 + #expect(r?.completed == 5) + #expect(r?.total == 20) + } + + @Test func simplify() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 3, total: 10) + + let r = (f1 + f2).simplified() + + #expect(r?.completed == 4) + #expect(r?.total == 5) + } + + @Test func overflow() { + // These prime numbers are problematic for overflowing + let denominators : [Int] = [5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 69] + + var f1 = ProgressFraction(completed: 1, total: 3) + for d in denominators { + f1 = f1 + ProgressFraction(completed: 1, total: d) + } + + let fractionResult = f1.fractionCompleted + var expectedResult = 1.0 / 3.0 + for d in denominators { + expectedResult = expectedResult + 1.0 / Double(d) + } + #expect(abs(fractionResult - expectedResult) < 0.00001) + } + + @Test func addOverflow() { + // These prime numbers are problematic for overflowing + let denominators : [Int] = [5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 69] + var f1 = ProgressFraction(completed: 1, total: 3) + for d in denominators { + f1 = f1 + ProgressFraction(completed: 1, total: d) + } + + // f1 should be in overflow + #expect(f1.overflowed) + + let f2 = ProgressFraction(completed: 1, total: 4) + f1 + + // f2 should also be in overflow + #expect(f2.overflowed) + + // And it should have completed value of about 1.0/4.0 + f1.fractionCompleted + let expected = (1.0 / 4.0) + f1.fractionCompleted + + #expect(abs(expected - f2.fractionCompleted) < 0.00001) + } + +#if _pointerBitWidth(_64) // These tests assumes Int is Int64 + @Test func addAndSubtractOverflow() { + let f1 = ProgressFraction(completed: 48, total: 60) + let f2 = ProgressFraction(completed: 5880, total: 7200) + let f3 = ProgressFraction(completed: 7048893638467736640, total: 8811117048084670800) + + let result1 = (f3 - f1) + f2 + #expect(result1.completed > 0) + + let result2 = (f3 - f2) + f1 + #expect(result2.completed < 60) + } + + @Test func subtractOverflow() { + let f1 = ProgressFraction(completed: 9855, total: 225066) + let f2 = ProgressFraction(completed: 14985363210613129, total: 56427817205760000) + + let result = f2 - f1 + #expect(abs(Double(result.completed) / Double(result.total!) - 0.2217) < 0.01) + } + + @Test func multiplyOverflow() { + let f1 = ProgressFraction(completed: 4294967279, total: 4294967291) + let f2 = ProgressFraction(completed: 4294967279, total: 4294967291) + + let result = f1 * f2 + #expect(abs(Double(result!.completed) / Double(result!.total!) - 1.0) < 0.01) + } +#endif + + @Test func fractionFromDouble() { + let d = 4.25 // exactly representable in binary + let f1 = ProgressFraction(double: d) + + let simplified = f1.simplified() + #expect(simplified?.completed == 17) + #expect(simplified?.total == 4) + } + + @Test func unnecessaryOverflow() { + // just because a fraction has a large denominator doesn't mean it needs to overflow + let f1 = ProgressFraction(completed: (Int.max - 1) / 2, total: Int.max - 1) + let f2 = ProgressFraction(completed: 1, total: 16) + + let r = f1 + f2 + #expect(!r.overflowed) + } +} diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerInteropTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerInteropTests.swift new file mode 100644 index 000000000..c8a459b3a --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerInteropTests.swift @@ -0,0 +1,332 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation + +/// Unit tests for interop methods that support building Progress trees with both Progress and ProgressManager +@Suite("Progress Manager Interop", .tags(.progressManager)) struct ProgressManagerInteropTests { + func doSomethingWithProgress() async -> Progress { + let p = Progress(totalUnitCount: 2) + return p + } + + func doSomething(subprogress: consuming Subprogress?) async { + let manager = subprogress?.start(totalCount: 4) + manager?.complete(count: 2) + manager?.complete(count: 2) + } + + // MARK: Progress - Subprogress Interop + @Test func interopProgressParentProgressManagerChild() async throws { + // Initialize a Progress Parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + // Check if Progress values propagate to Progress parent + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + // Add ProgressManager as Child + let p2 = overall.subprogress(assigningCount: 5) + await doSomething(subprogress: p2) + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + @Test func interopProgressParentProgressManagerGrandchild() async throws { + // Structure: Progress with two Progress children, one of the children has a ProgressManager child + let overall = Progress.discreteProgress(totalUnitCount: 10) + + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + let p2 = Progress(totalUnitCount: 1, parent: overall, pendingUnitCount: 5) + + await doSomething(subprogress: p2.subprogress(assigningCount: 1)) + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + @Test func interopProgressParentProgressManagerGrandchildAndProgressGrandchild() async throws { + // Structure: Progress with two Progress children, one of the children has a ProgressManager child and a Progress child + let overall = Progress.discreteProgress(totalUnitCount: 10) + + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + let p2 = Progress(totalUnitCount: 18) + overall.addChild(p2, withPendingUnitCount: 5) + + let p3 = await doSomethingWithProgress() + p2.addChild(p3, withPendingUnitCount: 9) + + let _ = await Task.detached { + p3.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p3.completedUnitCount = 2 + }.value + + await doSomething(subprogress: p2.subprogress(assigningCount: 9)) + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + // MARK: Progress - ProgressReporter Interop + @Test func interopProgressParentProgressReporterChild() async throws { + // Initialize a Progress parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + // Add ProgressReporter as Child + let p2 = ProgressManager(totalCount: 10) + let p2Reporter = p2.reporter + overall.addChild(p2Reporter, withPendingUnitCount: 5) + + p2.complete(count: 10) + + // Check if Progress values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + @Test func interopProgressParentProgressReporterChildWithNonZeroFractionCompleted() async throws { + // Initialize a Progress parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + // Add ProgressReporter with CompletedCount 3 as Child + let p2 = ProgressManager(totalCount: 10) + p2.complete(count: 3) + let p2Reporter = p2.reporter + overall.addChild(p2Reporter, withPendingUnitCount: 5) + + p2.complete(count: 7) + + // Check if Progress values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + @Test func interopProgressParentProgressReporterGrandchild() async throws { + // Initialize a Progress parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + let p2 = await doSomethingWithProgress() + overall.addChild(p2, withPendingUnitCount: 5) + + p2.completedUnitCount = 1 + + #expect(overall.fractionCompleted == 0.75) + #expect(overall.completedUnitCount == 5) + + // Add ProgressReporter as Child + let p3 = ProgressManager(totalCount: 10) + let p3Reporter = p3.reporter + p2.addChild(p3Reporter, withPendingUnitCount: 1) + + p3.complete(count: 10) + + // Check if Progress values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + // MARK: ProgressManager - Progress Interop + @Test func interopProgressManagerParentProgressChild() async throws { + // Initialize ProgressManager parent + let overallManager = ProgressManager(totalCount: 10) + + // Add ProgressManager as Child + await doSomething(subprogress: overallManager.subprogress(assigningCount: 5)) + + // Check if ProgressManager values propagate to ProgressManager parent + #expect(overallManager.fractionCompleted == 0.5) + #expect(overallManager.completedCount == 5) + + // Interop: Add Progress as Child + let p2 = await doSomethingWithProgress() + overallManager.assign(count: 5, to: p2) + + let _ = await Task.detached { + p2.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p2.completedUnitCount = 2 + }.value + + // Check if Progress values propagate to ProgressRerpoter parent + #expect(overallManager.completedCount == 10) + #expect(overallManager.totalCount == 10) + #expect(overallManager.fractionCompleted == 1.0) + } + + @Test func interopProgressManagerParentProgressGrandchild() async throws { + // Initialize ProgressManager parent + let overallManager = ProgressManager(totalCount: 10) + + // Add ProgressManager as Child + await doSomething(subprogress: overallManager.subprogress(assigningCount: 5)) + + #expect(overallManager.fractionCompleted == 0.5) + #expect(overallManager.completedCount == 5) + + let p2 = overallManager.subprogress(assigningCount: 5).start(totalCount: 3) + p2.complete(count: 1) + + + let p3 = await doSomethingWithProgress() + p2.assign(count: 2, to: p3) + + let _ = await Task.detached { + p3.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p3.completedUnitCount = 2 + }.value + + // Check if Progress values propagate to ProgressRerpoter parent + #expect(overallManager.completedCount == 10) + #expect(overallManager.fractionCompleted == 1.0) + } + + func getProgressWithTotalCountInitialized() -> Progress { + return Progress(totalUnitCount: 5) + } + + func receiveProgress(progress: consuming Subprogress) { + let _ = progress.start(totalCount: 5) + } + + // MARK: Behavior Consistency Tests + @Test func interopProgressManagerParentProgressChildConsistency() async throws { + let overallReporter = ProgressManager(totalCount: nil) + let child = overallReporter.subprogress(assigningCount: 5) + receiveProgress(progress: child) + #expect(overallReporter.totalCount == nil) + + let overallReporter2 = ProgressManager(totalCount: nil) + let interopChild = getProgressWithTotalCountInitialized() + overallReporter2.assign(count: 5, to: interopChild) + #expect(overallReporter2.totalCount == nil) + } + + @Test func interopProgressParentProgressManagerChildConsistency() async throws { + let overallProgress = Progress() + let child = Progress(totalUnitCount: 5) + overallProgress.addChild(child, withPendingUnitCount: 5) + #expect(overallProgress.totalUnitCount == 0) + + let overallProgress2 = Progress() + let interopChild = overallProgress2.subprogress(assigningCount: 5) + receiveProgress(progress: interopChild) + #expect(overallProgress2.totalUnitCount == 0) + } + + #if FOUNDATION_EXIT_TESTS + @Test func indirectParticipationOfProgressInAcyclicGraph() async throws { + await #expect(processExitsWith: .failure) { + let manager = ProgressManager(totalCount: 2) + + let parentManager1 = ProgressManager(totalCount: 1) + parentManager1.assign(count: 1, to: manager.reporter) + + let parentManager2 = ProgressManager(totalCount: 1) + parentManager2.assign(count: 1, to: manager.reporter) + + let progress = Progress.discreteProgress(totalUnitCount: 4) + manager.assign(count: 1, to: progress) + + progress.completedUnitCount = 2 + #expect(progress.fractionCompleted == 0.5) + #expect(manager.fractionCompleted == 0.25) + #expect(parentManager1.fractionCompleted == 0.25) + #expect(parentManager2.fractionCompleted == 0.25) + + progress.addChild(parentManager1.reporter, withPendingUnitCount: 1) + } + } + #endif +} +#endif diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift new file mode 100644 index 000000000..cbe6592b5 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -0,0 +1,1190 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +/// Unit tests for propagation of type-safe metadata in ProgressManager tree. +@Suite("Progress Manager File Properties", .tags(.progressManager)) struct ProgressManagerAdditionalPropertiesTests { + func doFileOperation(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 100) + manager.totalFileCount = 100 + + #expect(manager.totalFileCount == 100) + + manager.complete(count: 100) + #expect(manager.fractionCompleted == 1.0) + #expect(manager.isFinished == true) + + manager.completedFileCount = 100 + #expect(manager.completedFileCount == 100) + #expect(manager.totalFileCount == 100) + } + + @Test func discreteReporterWithFileProperties() async throws { + let fileProgressManager = ProgressManager(totalCount: 3) + await doFileOperation(reportTo: fileProgressManager.subprogress(assigningCount: 3)) + #expect(fileProgressManager.fractionCompleted == 1.0) + #expect(fileProgressManager.completedCount == 3) + #expect(fileProgressManager.isFinished == true) + #expect(fileProgressManager.totalFileCount == 0) + #expect(fileProgressManager.completedFileCount == 0) + + let summaryTotalFile = fileProgressManager.summary(of: \.totalFileCount) + #expect(summaryTotalFile == 100) + + let summaryCompletedFile = fileProgressManager.summary(of: \.completedFileCount) + #expect(summaryCompletedFile == 100) + } + + @Test func twoLevelTreeWithOneChildWithFileProperties() async throws { + let overall = ProgressManager(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let manager1 = progress1.start(totalCount: 10) + manager1.totalFileCount = 10 + manager1.completedFileCount = 0 + manager1.complete(count: 10) + + #expect(overall.fractionCompleted == 0.5) + + #expect(overall.totalFileCount == 0) + #expect(manager1.totalFileCount == 10) + #expect(manager1.completedFileCount == 0) + + let summaryTotalFile = overall.summary(of: \.totalFileCount) + #expect(summaryTotalFile == 10) + + let summaryCompletedFile = overall.summary(of: \.completedFileCount) + #expect(summaryCompletedFile == 0) + } + + @Test func twoLevelTreeWithTwoChildrenWithFileProperties() async throws { + let overall = ProgressManager(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let manager1 = progress1.start(totalCount: 10) + + manager1.totalFileCount = 11 + manager1.completedFileCount = 0 + + let progress2 = overall.subprogress(assigningCount: 1) + let manager2 = progress2.start(totalCount: 10) + + manager2.totalFileCount = 9 + manager2.completedFileCount = 0 + + #expect(overall.fractionCompleted == 0.0) + #expect(overall.totalFileCount == 0) + #expect(overall.completedFileCount == 0) + + let summaryTotalFile = overall.summary(of: \.totalFileCount) + #expect(summaryTotalFile == 20) + + let summaryCompletedFile = overall.summary(of: \.completedFileCount) + #expect(summaryCompletedFile == 0) + + // Update FileCounts + manager1.completedFileCount = 1 + + manager2.completedFileCount = 1 + + #expect(overall.completedFileCount == 0) + let summaryCompletedFileUpdated = overall.summary(of: \.completedFileCount) + #expect(summaryCompletedFileUpdated == 2) + } + + @Test func threeLevelTreeWithFileProperties() async throws { + let overall = ProgressManager(totalCount: 1) + + let progress1 = overall.subprogress(assigningCount: 1) + let manager1 = progress1.start(totalCount: 5) + + + let childProgress1 = manager1.subprogress(assigningCount: 3) + let childManager1 = childProgress1.start(totalCount: nil) + childManager1.totalFileCount += 10 + #expect(childManager1.totalFileCount == 10) + + let summaryTotalFileInitial = overall.summary(of: \.totalFileCount) + #expect(summaryTotalFileInitial == 10) + + let childProgress2 = manager1.subprogress(assigningCount: 2) + let childManager2 = childProgress2.start(totalCount: nil) + childManager2.totalFileCount += 10 + #expect(childManager2.totalFileCount == 10) + + // Tests that totalFileCount propagates to root level + #expect(overall.totalFileCount == 0) + let summaryTotalFile = overall.summary(of: \.totalFileCount) + #expect(summaryTotalFile == 20) + + manager1.totalFileCount += 999 + let summaryTotalFileUpdated = overall.summary(of: \.totalFileCount) + #expect(summaryTotalFileUpdated == 1019) + } +} + +@Suite("Progress Manager Byte Properties", .tags(.progressManager)) struct ProgressManagerBytePropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 3) + manager.totalByteCount = 300000 + + manager.complete(count: 1) + manager.completedByteCount += 100000 + + manager.complete(count: 1) + manager.completedByteCount += 100000 + + manager.complete(count: 1) + manager.completedByteCount += 100000 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.totalByteCount) == 300000) + #expect(manager.summary(of: \.completedByteCount) == 300000) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.totalByteCount = 200000 + manager.completedByteCount = 200000 + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.totalByteCount) == 500000) + #expect(manager.summary(of: \.completedByteCount) == 500000) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.totalByteCount = 2000 + manager.completedByteCount = 1000 + + #expect(manager.fractionCompleted == 0.5) + #expect(manager.summary(of: \.totalByteCount) == 2000) + #expect(manager.summary(of: \.completedByteCount) == 1000) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + manager.complete(count: 1) + manager.totalByteCount = 500000 + manager.completedByteCount = 499999 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.totalByteCount) == 800000) + #expect(manager.summary(of: \.completedByteCount) == 799999) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.totalByteCount = 100000 + manager.completedByteCount = 99999 + + try await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.totalByteCount) == 600000) + #expect(manager.summary(of: \.completedByteCount) == 599999) + } +} + +@Suite("Progress Manager Throughput Properties", .tags(.progressManager)) struct ProgressManagerThroughputTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + manager.complete(count: 1) + manager.throughput += 1000 + + manager.complete(count: 1) + manager.throughput += 1000 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.throughput) == [2000]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.throughput = 1000 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.throughput) == [1000, 2000]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.throughput = 1000 + manager.throughput += 2000 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.throughput) == [3000]) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + manager.complete(count: 1) + manager.throughput = 1000 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.throughput) == [1000, 2000]) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + manager.complete(count: 1) + + manager.throughput = 1000 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.throughput) == [1000, 1000, 2000]) + } +} + +@Suite("Progress Manager Estimated Time Remaining Properties", .tags(.progressManager)) struct ProgressManagerEstimatedTimeRemainingTests { + + func doSomething(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.estimatedTimeRemaining = Duration.seconds(3000) + + manager.complete(count: 1) + manager.estimatedTimeRemaining += Duration.seconds(3000) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.estimatedTimeRemaining) == Duration.seconds(6000)) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.estimatedTimeRemaining = Duration.seconds(1000) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.estimatedTimeRemaining) == Duration.seconds(1000)) + } + + @Test func twoLevelManagerWithFinishedChild() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.estimatedTimeRemaining = Duration.seconds(1) + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.estimatedTimeRemaining) == Duration.seconds(1)) + } + + @Test func twoLevelManagerWithUnfinishedChild() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.estimatedTimeRemaining = Duration.seconds(200) + + var child: ProgressManager? = manager.subprogress(assigningCount: 1).start(totalCount: 2) + child?.complete(count: 1) + child?.estimatedTimeRemaining = Duration.seconds(80000) + + #expect(manager.fractionCompleted == 0.75) + #expect(manager.summary(of: \.estimatedTimeRemaining) == Duration.seconds(80000)) + + child = nil + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.estimatedTimeRemaining) == Duration.seconds(200)) + } + +} + +extension ProgressManager.Properties { + + var counter: Counter.Type { Counter.self } + struct Counter: Sendable, ProgressManager.Property { + + typealias Value = Int + + typealias Summary = Int + + static var key: String { return "MyApp.Counter" } + + static var defaultValue: Int { return 0 } + + static var defaultSummary: Int { return 0 } + + static func reduce(into summary: inout Int, value: Int) { + summary += value + } + + static func merge(_ summary1: Int, _ summary2: Int) -> Int { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: Int, _ childSummary: Int) -> Int { + return parentSummary + childSummary + } + } +} + +@Suite("Progress Manager Int Properties", .tags(.progressManager)) struct ProgressManagerIntPropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.counter += 10 + + manager.complete(count: 1) + manager.counter += 10 + + manager.complete(count: 1) + manager.counter += 10 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.counter) == 30) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + + manager.counter = 15 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.counter) == 45) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.counter += 10 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.counter) == 10) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.counter += 10 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.counter) == 40) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.counter += 10 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.counter) == 55) + } +} + +extension ProgressManager.Properties { + var byteSize: ByteSize.Type { ByteSize.self } + struct ByteSize: Sendable, ProgressManager.Property { + + typealias Value = UInt64 + + typealias Summary = UInt64 + + static var key: String { return "MyApp.ByteSize" } + + static var defaultValue: UInt64 { return 0 } + + static var defaultSummary: UInt64 { return 0 } + + static func reduce(into summary: inout UInt64, value: UInt64) { + summary += value + } + + static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: UInt64, _ childSummary: UInt64) -> UInt64 { + return parentSummary + childSummary + } + } +} + + +@Suite("Progress Manager UInt64 Properties", .tags(.progressManager)) struct ProgressManagerUInt64PropertiesTests { + +func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.byteSize += 1024 + + manager.complete(count: 1) + manager.byteSize += 2048 + + manager.complete(count: 1) + manager.byteSize += 4096 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.byteSize) == 7168) +} + +func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + + manager.byteSize = 8192 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.byteSize) == 15360) +} + +@Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.byteSize += 16384 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.byteSize) == 16384) +} + +@Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.byteSize += 32768 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.byteSize) == 39936) +} + +@Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.byteSize += 65536 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.byteSize) == 80896) +} +} + +extension ProgressManager.Properties { + + var justADouble: JustADouble.Type { JustADouble.self } + struct JustADouble: Sendable, ProgressManager.Property { + + typealias Value = Double + + typealias Summary = Double + + static var key: String { return "MyApp.JustADouble" } + + static var defaultValue: Double { return 0.0 } + + static var defaultSummary: Double { return 0.0 } + + static func reduce(into summary: inout Double, value: Double) { + summary += value + } + + static func merge(_ summary1: Double, _ summary2: Double) -> Double { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: Double, _ childSummary: Double) -> Double { + return parentSummary + childSummary + } + } +} + +@Suite("Progress Manager Double Properties", .tags(.progressManager)) struct ProgressManagerDoublePropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.justADouble += 10.0 + + manager.complete(count: 1) + manager.justADouble += 10.0 + + manager.complete(count: 1) + manager.justADouble += 10.0 + + #expect(manager.summary(of: \.justADouble) == 30.0) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.justADouble = 7.0 + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.summary(of: \.justADouble) == 37.0) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.justADouble = 80.0 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.justADouble) == 80.0) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.justADouble = 80.0 + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.justADouble) == 110.0) + } + + @Test func threeLevelManager() async throws { + + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.justADouble = 80.0 + + try await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.justADouble) == 117.0) + } +} + +extension ProgressManager.Properties { + + var downloadedFile: DownloadedFile.Type { DownloadedFile.self } + struct DownloadedFile: Sendable, ProgressManager.Property { + + typealias Value = String? + + typealias Summary = [String?] + + static var key: String { return "FileName" } + + static var defaultValue: String? { return "" } + + static var defaultSummary: [String?] { return [] } + + static func reduce(into summary: inout [String?], value: String?) { + summary.append(value) + } + + static func merge(_ summary1: [String?], _ summary2: [String?]) -> [String?] { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: [String?], _ childSummary: [String?]) -> [String?] { + return parentSummary + childSummary + } + } +} + + +@Suite("Progress Manager String (Retaining) Properties", .tags(.progressManager)) struct ProgressManagerStringPropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.complete(count: 1) + manager.downloadedFile = "Melon.jpg" + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.downloadedFile) == ["Melon.jpg"]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.downloadedFile = "Cherry.jpg" + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.downloadedFile) == ["Cherry.jpg", "Melon.jpg"]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.downloadedFile = "Grape.jpg" + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.downloadedFile == "Grape.jpg") + #expect(manager.summary(of: \.downloadedFile) == ["Grape.jpg"]) + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.downloadedFile = "Watermelon.jpg" + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.downloadedFile) == ["Watermelon.jpg", "Melon.jpg"]) + } + + @Test func threeLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.downloadedFile = "Watermelon.jpg" + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.downloadedFile) == ["Watermelon.jpg", "Cherry.jpg", "Melon.jpg"]) + } +} + +extension ProgressManager.Properties { + + var processingFile: ProcessingFile.Type { ProcessingFile.self } + struct ProcessingFile: Sendable, ProgressManager.Property { + + typealias Value = String? + + typealias Summary = [String?] + + static var key: String { return "MyApp.ProcessingFile" } + + static var defaultValue: String? { return "" } + + static var defaultSummary: [String?] { return [] } + + static func reduce(into summary: inout [String?], value: String?) { + summary.append(value) + } + + static func merge(_ summary1: [String?], _ summary2: [String?]) -> [String?] { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: [String?], _ childSummary: [String?]) -> [String?] { + return parentSummary + } + } +} + +@Suite("Progress Manager String (Non-retaining) Properties", .tags(.progressManager)) struct ProgressManagerStringNonRetainingProperties { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.complete(count: 1) + manager.processingFile = "Hello.jpg" + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processingFile) == ["Hello.jpg"]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.processingFile = "Hi.jpg" + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processingFile) == ["Hi.jpg"]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.processingFile = "Howdy.jpg" + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.processingFile == "Howdy.jpg") + #expect(manager.summary(of: \.processingFile) == ["Howdy.jpg"]) + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processingFile = "Howdy.jpg" + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processingFile) == ["Howdy.jpg"]) + } + + @Test func threeLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processingFile = "Howdy.jpg" + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processingFile) == ["Howdy.jpg"]) + } +} + + +extension ProgressManager.Properties { + var imageURL: ImageURL.Type { ImageURL.self } + struct ImageURL: Sendable, ProgressManager.Property { + + typealias Value = URL? + + typealias Summary = [URL?] + + static var key: String { "MyApp.ImageURL" } + + static var defaultValue: URL? { nil } + + static var defaultSummary: [URL?] { [] } + + static func reduce(into summary: inout [URL?], value: URL?) { + summary.append(value) + } + + static func merge(_ summary1: [URL?], _ summary2: [URL?]) -> [URL?] { + summary1 + summary2 + } + + static func finalSummary(_ parentSummary: [URL?], _ childSummary: [URL?]) -> [URL?] { + parentSummary + } + } +} + +@Suite("Progress Manager URL (Non-retaining) Properties", .tags(.progressManager)) struct ProgressManagerURLProperties { + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.complete(count: 1) + manager.imageURL = URL(string: "112.jpg") + + #expect(manager.summary(of: \.imageURL) == [URL(string: "112.jpg")]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.imageURL = URL(string: "114.jpg") + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.summary(of: \.imageURL) == [URL(string: "114.jpg")]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.imageURL = URL(string: "116.jpg") + + #expect(manager.fractionCompleted == 0.0) + #expect(manager.summary(of: \.imageURL) == [URL(string: "116.jpg")]) + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.imageURL = URL(string: "116.jpg") + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.imageURL) == [URL(string: "116.jpg")]) + } + + @Test func threeLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.imageURL = URL(string: "116.jpg") + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.imageURL) == [URL(string: "116.jpg")]) + } +} + +extension ProgressManager.Properties { + var totalPixelCount: TotalPixelCount.Type { TotalPixelCount.self } + struct TotalPixelCount: Sendable, ProgressManager.Property { + typealias Value = UInt64 + + typealias Summary = [UInt64] + + static var key: String { "MyApp.TotalPixelCount" } + + static var defaultValue: UInt64 { 0 } + + static var defaultSummary: [UInt64] { [] } + + static func reduce(into summary: inout [UInt64], value: UInt64) { + summary.append(value) + } + + static func merge(_ summary1: [UInt64], _ summary2: [UInt64]) -> [UInt64] { + summary1 + summary2 + } + + static func finalSummary(_ parentSummary: [UInt64], _ childSummary: [UInt64]) -> [UInt64] { + parentSummary + childSummary + } + } +} + +@Suite("Progress Manager UInt64 Array (Retaining) Properties", .tags(.progressManager)) struct ProgressManagerUInt64ArrayProperties { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.complete(count: 1) + manager.totalPixelCount = 24 + + #expect(manager.summary(of: \.totalPixelCount) == [24]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.totalPixelCount = 26 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.summary(of: \.totalPixelCount) == [26, 24]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.totalPixelCount = 42 + + #expect(manager.fractionCompleted == 0.0) + #expect(manager.summary(of: \.totalPixelCount) == [42]) + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.totalPixelCount = 42 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.totalPixelCount) == [42, 24]) + } + + @Test func threeLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.totalPixelCount = 42 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.totalPixelCount) == [42, 26, 24]) + } +} + +extension ProgressManager.Properties { + var viralIndeterminate: ViralIndeterminate.Type { ViralIndeterminate.self } + struct ViralIndeterminate: Sendable, ProgressManager.Property { + typealias Value = Int + + typealias Summary = Int + + static var key: String { "MyApp.ViralIndeterminate" } + + static var defaultValue: Int { 1 } + + static var defaultSummary: Int { 1 } + + static func reduce(into summary: inout Int, value: Int) { + summary = min(summary, value) + } + + static func merge(_ summary1: Int, _ summary2: Int) -> Int { + min(summary1, summary2) + } + + static func finalSummary(_ parentSummary: Int, _ childSummary: Int) -> Int { + min(parentSummary, childSummary) + } + } +} + + +@Suite("Progress Manager Viral Indeterminate Property", .tags(.progressManager)) struct ProgressManagerViralIndeterminateProperties { + // Tests the use of additional property to virally propagate property from leaf to root + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.viralIndeterminate = 0 + + manager.complete(count: 1) + + manager.complete(count: 1) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.viralIndeterminate) == 0) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.viralIndeterminate = 1 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.viralIndeterminate) == 0) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.viralIndeterminate = 1 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.viralIndeterminate) == 1) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.viralIndeterminate = 1 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.viralIndeterminate) == 0) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.viralIndeterminate = 1 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.viralIndeterminate) == 0) + } +} + +extension ProgressManager.Properties { + var processTime: ProcessTime.Type { ProcessTime.self } + struct ProcessTime: Sendable, ProgressManager.Property { + + typealias Value = Duration + + typealias Summary = Duration + + static var key: String { return "MyApp.ProcessTime" } + + static var defaultValue: Duration { return .zero } + + static var defaultSummary: Duration { return .zero } + + static func reduce(into summary: inout Duration, value: Duration) { + summary += value + } + + static func merge(_ summary1: Duration, _ summary2: Duration) -> Duration { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: Duration, _ childSummary: Duration) -> Duration { + return parentSummary + childSummary + } + } +} + +@Suite("Progress Manager Duration Properties", .tags(.progressManager)) struct ProgressManagerDurationPropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.processTime += Duration.seconds(10) + + manager.complete(count: 1) + manager.processTime += Duration.seconds(15) + + manager.complete(count: 1) + manager.processTime += Duration.seconds(25) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processTime) == Duration.seconds(50)) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + + manager.processTime = Duration.seconds(30) + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processTime) == Duration.seconds(80)) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.processTime += Duration.milliseconds(500) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processTime) == Duration.milliseconds(500)) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processTime += Duration.seconds(120) + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processTime) == Duration.seconds(170)) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processTime += Duration.microseconds(1000000) // 1 second + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processTime) == Duration.seconds(81)) + } + + @Test func zeroDurationHandling() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processTime = Duration.zero + + let childProgress = manager.subprogress(assigningCount: 1) + let childManager = childProgress.start(totalCount: 1) + + childManager.complete(count: 1) + childManager.processTime = Duration.seconds(42) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processTime) == Duration.seconds(42)) + } + + @Test func negativeDurationHandling() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + // Test with negative duration (though this might be unusual in practice) + manager.processTime = Duration.seconds(-5) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processTime) == Duration.seconds(-5)) + } + + @Test func mixedDurationUnits() async throws { + let manager = ProgressManager(totalCount: 3) + + manager.complete(count: 1) + manager.processTime = Duration.seconds(1) // 1 second + + manager.complete(count: 1) + manager.processTime += Duration.milliseconds(500) // + 0.5 seconds + + manager.complete(count: 1) + manager.processTime += Duration.microseconds(500000) // + 0.5 seconds + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: \.processTime) == Duration.seconds(2)) + } +} + diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift new file mode 100644 index 000000000..3fdc7cd33 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -0,0 +1,795 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +extension Tag { + @Tag static var progressManager: Self +} + +/// Unit tests for basic functionalities of ProgressManager +@Suite("Progress Manager", .tags(.progressManager)) struct ProgressManagerTests { + /// MARK: Helper methods that report progress + func doBasicOperationV1(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 8) + for i in 1...8 { + manager.complete(count: 1) + #expect(manager.completedCount == i) + #expect(manager.fractionCompleted == Double(i) / Double(8)) + } + } + + func doBasicOperationV2(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 7) + for i in 1...7 { + manager.complete(count: 1) + #expect(manager.completedCount == i) + #expect(manager.fractionCompleted == Double(i) / Double(7)) + } + } + + func doBasicOperationV3(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 11) + for i in 1...11 { + manager.complete(count: 1) + #expect(manager.completedCount == i) + #expect(manager.fractionCompleted == Double(i) / Double(11)) + } + } + + /// MARK: Tests calculations based on change in totalCount + @Test func totalCountNil() async throws { + let overall = ProgressManager(totalCount: nil) + overall.complete(count: 10) + #expect(overall.completedCount == 10) + #expect(overall.fractionCompleted == 0.0) + #expect(overall.isIndeterminate == true) + #expect(overall.totalCount == nil) + } + + @Test func totalCountReset() async throws { + let overall = ProgressManager(totalCount: 10) + overall.complete(count: 5) + #expect(overall.completedCount == 5) + #expect(overall.totalCount == 10) + #expect(overall.fractionCompleted == 0.5) + #expect(overall.isIndeterminate == false) + + overall.setCounts { _, total in + total = nil + } + overall.complete(count: 1) + #expect(overall.completedCount == 6) + #expect(overall.totalCount == nil) + #expect(overall.fractionCompleted == 0.0) + #expect(overall.isIndeterminate == true) + #expect(overall.isFinished == false) + + overall.setCounts { _, total in + total = 12 + } + overall.complete(count: 2) + #expect(overall.completedCount == 8) + #expect(overall.totalCount == 12) + #expect(overall.fractionCompleted == Double(8) / Double(12)) + #expect(overall.isIndeterminate == false) + #expect(overall.isFinished == false) + } + + @Test func totalCountNilWithChild() async throws { + let overall = ProgressManager(totalCount: nil) + #expect(overall.completedCount == 0) + #expect(overall.totalCount == nil) + #expect(overall.fractionCompleted == 0.0) + #expect(overall.isIndeterminate == true) + #expect(overall.isFinished == false) + + let progress1 = overall.subprogress(assigningCount: 2) + let manager1 = progress1.start(totalCount: 1) + + manager1.complete(count: 1) + #expect(manager1.totalCount == 1) + #expect(manager1.completedCount == 1) + #expect(manager1.fractionCompleted == 1.0) + #expect(manager1.isIndeterminate == false) + #expect(manager1.isFinished == true) + + #expect(overall.completedCount == 2) + #expect(overall.totalCount == nil) + #expect(overall.fractionCompleted == 0.0) + #expect(overall.isIndeterminate == true) + #expect(overall.isFinished == false) + + overall.setCounts { _, total in + total = 5 + } + #expect(overall.completedCount == 2) + #expect(overall.totalCount == 5) + #expect(overall.fractionCompleted == 0.4) + #expect(overall.isIndeterminate == false) + #expect(overall.isFinished == false) + } + + @Test func totalCountFinishesWithLessCompletedCount() async throws { + let overall = ProgressManager(totalCount: 10) + overall.complete(count: 5) + + let progress1 = overall.subprogress(assigningCount: 8) + let manager1 = progress1.start(totalCount: 1) + manager1.complete(count: 1) + + #expect(overall.completedCount == 13) + #expect(overall.totalCount == 10) + #expect(overall.fractionCompleted == 1.3) + #expect(overall.isIndeterminate == false) + #expect(overall.isFinished == true) + } + + @Test func childTotalCountReset() async throws { + let overall = ProgressManager(totalCount: 1) + + let childManager = overall.subprogress(assigningCount: 1).start(totalCount: 4) + childManager.complete(count: 2) + + #expect(overall.fractionCompleted == 0.5) + #expect(childManager.isIndeterminate == false) + + childManager.setCounts { _, total in + total = nil + } + + #expect(overall.fractionCompleted == 0.0) + #expect(childManager.isIndeterminate == true) + #expect(childManager.completedCount == 2) + + childManager.setCounts { _, total in + total = 5 + } + childManager.complete(count: 2) + + #expect(overall.fractionCompleted == 0.8) + #expect(childManager.completedCount == 4) + #expect(childManager.isIndeterminate == false) + + childManager.complete(count: 1) + #expect(overall.fractionCompleted == 1.0) + } + + /// MARK: Tests single-level tree + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 3) + await doBasicOperationV1(reportTo: manager.subprogress(assigningCount: 3)) + #expect(manager.fractionCompleted == 1.0) + #expect(manager.completedCount == 3) + #expect(manager.isFinished == true) + } + + /// MARK: Tests multiple-level trees + @Test func emptyDiscreteManager() async throws { + let manager = ProgressManager(totalCount: nil) + #expect(manager.isIndeterminate == true) + + manager.setCounts { _, total in + total = 10 + } + #expect(manager.isIndeterminate == false) + #expect(manager.totalCount == 10) + + await doBasicOperationV1(reportTo: manager.subprogress(assigningCount: 10)) + #expect(manager.fractionCompleted == 1.0) + #expect(manager.completedCount == 10) + #expect(manager.isFinished == true) + } + + @Test func twoLevelTreeWithTwoChildren() async throws { + let overall = ProgressManager(totalCount: 2) + + await doBasicOperationV1(reportTo: overall.subprogress(assigningCount: 1)) + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedCount == 1) + #expect(overall.isFinished == false) + #expect(overall.isIndeterminate == false) + + await doBasicOperationV2(reportTo: overall.subprogress(assigningCount: 1)) + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedCount == 2) + #expect(overall.isFinished == true) + #expect(overall.isIndeterminate == false) + } + + @Test func twoLevelTreeWithTwoChildrenWithOneFileProperty() async throws { + let overall = ProgressManager(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let manager1 = progress1.start(totalCount: 5) + manager1.complete(count: 5) + + let progress2 = overall.subprogress(assigningCount: 1) + let manager2 = progress2.start(totalCount: 5) + manager2.totalFileCount = 10 + + #expect(overall.fractionCompleted == 0.5) + // Parent is expected to get totalFileCount from one of the children with a totalFileCount + #expect(overall.totalFileCount == 0) + } + + @Test func twoLevelTreeWithMultipleChildren() async throws { + let overall = ProgressManager(totalCount: 3) + + await doBasicOperationV1(reportTo: overall.subprogress(assigningCount:1)) + #expect(overall.fractionCompleted == Double(1) / Double(3)) + #expect(overall.completedCount == 1) + + await doBasicOperationV2(reportTo: overall.subprogress(assigningCount:1)) + #expect(overall.fractionCompleted == Double(2) / Double(3)) + #expect(overall.completedCount == 2) + + await doBasicOperationV3(reportTo: overall.subprogress(assigningCount:1)) + #expect(overall.fractionCompleted == Double(3) / Double(3)) + #expect(overall.completedCount == 3) + } + + @Test func threeLevelTree() async throws { + let overall = ProgressManager(totalCount: 100) + #expect(overall.fractionCompleted == 0.0) + + let child1 = overall.subprogress(assigningCount: 100) + let manager1 = child1.start(totalCount: 100) + + let grandchild1 = manager1.subprogress(assigningCount: 100) + let grandchildManager1 = grandchild1.start(totalCount: 100) + + #expect(overall.fractionCompleted == 0.0) + + grandchildManager1.complete(count: 50) + #expect(manager1.fractionCompleted == 0.5) + #expect(overall.fractionCompleted == 0.5) + + grandchildManager1.complete(count: 50) + #expect(manager1.fractionCompleted == 1.0) + #expect(overall.fractionCompleted == 1.0) + + #expect(grandchildManager1.isFinished == true) + #expect(manager1.isFinished == true) + #expect(overall.isFinished == true) + } + + @Test func fourLevelTree() async throws { + let overall = ProgressManager(totalCount: 100) + #expect(overall.fractionCompleted == 0.0) + + let child1 = overall.subprogress(assigningCount: 100) + let manager1 = child1.start(totalCount: 100) + + let grandchild1 = manager1.subprogress(assigningCount: 100) + let grandchildManager1 = grandchild1.start(totalCount: 100) + + #expect(overall.fractionCompleted == 0.0) + + let greatGrandchild1 = grandchildManager1.subprogress(assigningCount: 100) + let greatGrandchildManager1 = greatGrandchild1.start(totalCount: 100) + + greatGrandchildManager1.complete(count: 50) + #expect(overall.fractionCompleted == 0.5) + + greatGrandchildManager1.complete(count: 50) + #expect(overall.fractionCompleted == 1.0) + + #expect(greatGrandchildManager1.isFinished == true) + #expect(grandchildManager1.isFinished == true) + #expect(manager1.isFinished == true) + #expect(overall.isFinished == true) + } + + func doSomething(amount: Int, subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: amount) + for _ in 1...amount { + manager.complete(count: 1) + } + } + + @Test func fiveThreadsMutatingAndReading() async throws { + let manager = ProgressManager(totalCount: 10) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + await doSomething(amount: 5, subprogress: manager.subprogress(assigningCount: 1)) + } + + group.addTask { + await doSomething(amount: 8, subprogress: manager.subprogress(assigningCount: 1)) + } + + group.addTask { + await doSomething(amount: 7, subprogress: manager.subprogress(assigningCount: 1)) + } + + group.addTask { + await doSomething(amount: 6, subprogress: manager.subprogress(assigningCount: 1)) + } + + group.addTask { + #expect(manager.fractionCompleted <= 0.4) + } + } + } + + // MARK: Test deinit behavior + func makeUnfinishedChild(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + manager.complete(count: 2) + #expect(manager.fractionCompleted == Double(2) / Double(3)) + } + + func makeFinishedChild(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + manager.complete(count: 2) + } + + @Test func unfinishedChild() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + #expect(manager.fractionCompleted == 0.5) + + await makeUnfinishedChild(subprogress: manager.subprogress(assigningCount: 1)) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func unfinishedGrandchild() async throws { + let manager = ProgressManager(totalCount: 1) + + let child = manager.subprogress(assigningCount: 1).start(totalCount: 1) + + await makeUnfinishedChild(subprogress: child.subprogress(assigningCount: 1)) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func unfinishedGreatGrandchild() async throws { + let manager = ProgressManager(totalCount: 1) + + let child = manager.subprogress(assigningCount: 1).start(totalCount: 1) + + let grandchild = child.subprogress(assigningCount: 1).start(totalCount: 1) + + await makeUnfinishedChild(subprogress: grandchild.subprogress(assigningCount: 1)) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func finishedChildUnreadBeforeDeinit() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + #expect(manager.fractionCompleted == 0.5) + + await makeFinishedChild(subprogress: manager.subprogress(assigningCount: 1)) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func finishedChildReadBeforeDeinit() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + #expect(manager.fractionCompleted == 0.5) + + var child: ProgressManager? = manager.subprogress(assigningCount: 1).start(totalCount: 1) + child?.complete(count: 1) + #expect(manager.fractionCompleted == 1.0) + + child = nil + #expect(manager.fractionCompleted == 1.0) + } + + @Test func uninitializedSubprogress() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + + var subprogress: Subprogress? = manager.subprogress(assigningCount: 1) + #expect(manager.fractionCompleted == 0.5) + + subprogress = nil + #expect(manager.fractionCompleted == 1.0) + } + + @Test func deallocatedChild() async throws { + let manager = ProgressManager(totalCount: 100) + + var child: ProgressManager? = manager.subprogress(assigningCount: 50).start(totalCount: 10) + child!.complete(count: 5) + + let fractionBeforeDeallocation = manager.fractionCompleted + #expect(fractionBeforeDeallocation == 0.25) + + child = nil + + for _ in 1...10 { + _ = manager.fractionCompleted + } + + let fractionAfterDeallocation = manager.fractionCompleted + + #expect(fractionAfterDeallocation == 0.5, "Deallocated child should be assumed completed.") + + manager.complete(count: 50) + #expect(manager.fractionCompleted == 1.0) + } +} + +// MARK: - Thread Safety and Concurrent Access Tests +@Suite("Progress Manager Thread Safety Tests", .tags(.progressManager)) struct ProgressManagerThreadSafetyTests { + + @Test func concurrentBasicPropertiesAccess() async throws { + let manager = ProgressManager(totalCount: 10) + manager.complete(count: 5) + + await withThrowingTaskGroup(of: Void.self) { group in + + group.addTask { + for _ in 1...10 { + let fraction = manager.fractionCompleted + #expect(fraction == 0.5) + } + } + + group.addTask { + for _ in 1...10 { + let completed = manager.completedCount + #expect(completed == 5) + } + } + + group.addTask { + for _ in 1...10 { + let total = manager.totalCount + #expect(total == 10) + } + } + + group.addTask { + for _ in 1...10 { + let isFinished = manager.isFinished + #expect(isFinished == false) + } + } + + group.addTask { + for _ in 1...10 { + let isIndeterminate = manager.isIndeterminate + #expect(isIndeterminate == false) + } + } + } + } + + @Test func concurrentMultipleChildrenUpdatesAndParentReads() async throws { + let manager = ProgressManager(totalCount: 100) + let child1 = manager.subprogress(assigningCount: 30).start(totalCount: 10) + let child2 = manager.subprogress(assigningCount: 40).start(totalCount: 8) + let child3 = manager.subprogress(assigningCount: 30).start(totalCount: 6) + + await withTaskGroup(of: Void.self) { group in + group.addTask { + for _ in 1...10 { + child1.complete(count: 1) + } + } + + group.addTask { + for _ in 1...8 { + child2.complete(count: 1) + } + } + + group.addTask { + for _ in 1...6 { + child3.complete(count: 1) + } + } + + group.addTask { + for _ in 1...50 { + let _ = manager.fractionCompleted + let _ = manager.completedCount + let _ = manager.isFinished + } + } + + group.addTask { + for _ in 1...30 { + let _ = child1.fractionCompleted + let _ = child2.completedCount + let _ = child3.isFinished + } + } + } + + #expect(child1.isFinished == true) + #expect(child2.isFinished == true) + #expect(child3.isFinished == true) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func concurrentSingleChildUpdatesAndParentReads() async throws { + let manager = ProgressManager(totalCount: 50) + let child = manager.subprogress(assigningCount: 50).start(totalCount: 100) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for i in 1...100 { + child.complete(count: 1) + if i % 10 == 0 { + try? await Task.sleep(nanoseconds: 1_000_000) + } + } + } + + group.addTask { + for _ in 1...200 { + let _ = manager.fractionCompleted + let _ = manager.completedCount + let _ = manager.totalCount + let _ = manager.isFinished + let _ = manager.isIndeterminate + } + } + + group.addTask { + for _ in 1...150 { + let _ = child.fractionCompleted + let _ = child.completedCount + let _ = child.isFinished + } + } + } + + #expect(child.isFinished == true) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func concurrentGrandchildrenUpdates() async throws { + let parent = ProgressManager(totalCount: 60) + let child1 = parent.subprogress(assigningCount: 20).start(totalCount: 10) + let child2 = parent.subprogress(assigningCount: 20).start(totalCount: 8) + let child3 = parent.subprogress(assigningCount: 20).start(totalCount: 6) + + let grandchild1 = child1.subprogress(assigningCount: 5).start(totalCount: 4) + let grandchild2 = child2.subprogress(assigningCount: 4).start(totalCount: 3) + let grandchild3 = child3.subprogress(assigningCount: 3).start(totalCount: 2) + + await withTaskGroup(of: Void.self) { group in + group.addTask { + for _ in 1...4 { + grandchild1.complete(count: 1) + } + } + + group.addTask { + for _ in 1...3 { + grandchild2.complete(count: 1) + } + } + + group.addTask { + for _ in 1...2 { + grandchild3.complete(count: 1) + } + } + + group.addTask { + for _ in 1...5 { + child1.complete(count: 1) + } + } + + group.addTask { + for _ in 1...4 { + child2.complete(count: 1) + } + } + + group.addTask { + for _ in 1...3 { + child3.complete(count: 1) + } + } + + group.addTask { + for _ in 1...100 { + let _ = parent.fractionCompleted + let _ = child1.fractionCompleted + let _ = grandchild1.completedCount + } + } + } + + #expect(grandchild1.isFinished == true) + #expect(grandchild2.isFinished == true) + #expect(grandchild3.isFinished == true) + #expect(parent.isFinished == true) + } + + @Test func concurrentReadDuringIndeterminateToDeterminateTransition() async throws { + let manager = ProgressManager(totalCount: nil) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for _ in 1...50 { + let _ = manager.fractionCompleted + let _ = manager.isIndeterminate + } + } + + group.addTask { + for _ in 1...10 { + manager.complete(count: 1) + } + } + + // Task 3: Change to determinate after a delay + group.addTask { + try? await Task.sleep(nanoseconds: 1_000_000) + manager.setCounts { _, total in + total = 20 + } + + for _ in 1...30 { + let _ = manager.fractionCompleted + let _ = manager.isIndeterminate + } + } + } + + #expect(manager.totalCount == 20) + #expect(manager.completedCount == 10) + #expect(manager.isIndeterminate == false) + } + + @Test func concurrentReadDuringExcessiveCompletion() async throws { + let manager = ProgressManager(totalCount: 5) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for _ in 1...20 { + manager.complete(count: 1) + try? await Task.sleep(nanoseconds: 100_000) + } + } + + group.addTask { + for _ in 1...100 { + let fraction = manager.fractionCompleted + let completed = manager.completedCount + + #expect(completed >= 0 && completed <= 20) + #expect(fraction >= 0.0 && fraction <= 4.0) + } + } + } + + #expect(manager.completedCount == 20) + #expect(manager.fractionCompleted == 4.0) + #expect(manager.isFinished == true) + } + + @Test func concurrentChildrenDeinitializationAndParentReads() async throws { + let manager = ProgressManager(totalCount: 100) + + await withThrowingTaskGroup(of: Void.self) { group in + // Create and destroy children rapidly + for batch in 1...10 { + group.addTask { + for i in 1...5 { + func createAndDestroyChild() { + let child = manager.subprogress(assigningCount: 2).start(totalCount: 3) + child.complete(count: 2 + (i % 2)) // Complete 2 or 3 + // child deinits here + } + + createAndDestroyChild() + try? await Task.sleep(nanoseconds: 200_000 * UInt64(batch)) + } + } + } + + // Continuously read manager state during child lifecycle + group.addTask { + for _ in 1...300 { + let fraction = manager.fractionCompleted + let completed = manager.completedCount + + // Properties should be stable and valid + #expect(fraction >= 0.0) + #expect(completed >= 0) + + try? await Task.sleep(nanoseconds: 50_000) + } + } + } + + // Manager should reach completion + #expect(manager.fractionCompleted == 1.0) + #expect(manager.completedCount == 100) + } + + @Test func concurrentReadAndWriteAndCycleDetection() async throws { + let manager1 = ProgressManager(totalCount: 10) + let manager2 = ProgressManager(totalCount: 10) + let manager3 = ProgressManager(totalCount: 10) + + // Create initial chain: manager1 -> manager2 -> manager3 + manager1.assign(count: 5, to: manager2.reporter) + manager2.assign(count: 5, to: manager3.reporter) + + await withTaskGroup(of: Void.self) { group in + // Task 1: Try to detect cycles continuously + group.addTask { + for _ in 1...50 { + let wouldCycle1 = manager1.isCycle(reporter: manager3.reporter) + let wouldCycle2 = manager2.isCycle(reporter: manager1.reporter) + let wouldCycle3 = manager3.isCycle(reporter: manager2.reporter) + + #expect(wouldCycle1 == false) // No cycle yet + #expect(wouldCycle2 == true) // Would create cycle + #expect(wouldCycle3 == true) // Would create cycle + } + } + + // Task 2: Complete work in all managers + group.addTask { + for _ in 1...5 { + manager1.complete(count: 1) + manager2.complete(count: 1) + manager3.complete(count: 1) + } + } + + // Task 3: Access properties during cycle detection + group.addTask { + for _ in 1...100 { + let _ = manager1.fractionCompleted + let _ = manager2.completedCount + let _ = manager3.isFinished + } + } + } + } + + @Test func concurrentSubprogressCreation() async throws { + let manager = ProgressManager(totalCount: 1000) + + await withThrowingTaskGroup(of: Void.self) { group in + // Create 20 concurrent tasks, each creating multiple subprogresses + for _ in 1...20 { + group.addTask { + for i in 1...10 { + let child = manager.subprogress(assigningCount: 5).start(totalCount: 4) + child.complete(count: 4) + + // Immediately access properties + let _ = child.fractionCompleted + let _ = manager.fractionCompleted + + try? await Task.sleep(nanoseconds: 100_000 * UInt64(i)) + } + } + } + } + + #expect(manager.completedCount == 1000) + } +} diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift new file mode 100644 index 000000000..f41aaae78 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift @@ -0,0 +1,150 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +@Suite("Progress Reporter", .tags(.progressManager)) struct ProgressReporterTests { + @Test func observeProgressReporter() { + let manager = ProgressManager(totalCount: 3) + + let reporter = manager.reporter + + manager.complete(count: 1) + #expect(reporter.completedCount == 1) + + manager.complete(count: 1) + #expect(reporter.completedCount == 2) + + manager.complete(count: 1) + #expect(reporter.completedCount == 3) + + let fileCount = reporter.totalFileCount + #expect(fileCount == 0) + + manager.totalFileCount = 6 + #expect(reporter.totalFileCount == 6) + + let summaryTotalFile = manager.summary(of: \.totalFileCount) + #expect(summaryTotalFile == 6) + } + + @Test func testAddProgressReporterAsChild() { + let manager = ProgressManager(totalCount: 2) + + let reporter = manager.reporter + + let altManager1 = ProgressManager(totalCount: 4) + altManager1.assign(count: 1, to: reporter) + + let altManager2 = ProgressManager(totalCount: 5) + altManager2.assign(count: 2, to: reporter) + + manager.complete(count: 1) + #expect(altManager1.fractionCompleted == 0.125) + #expect(altManager2.fractionCompleted == 0.2) + + manager.complete(count: 1) + #expect(altManager1.fractionCompleted == 0.25) + #expect(altManager2.fractionCompleted == 0.4) + } + + @Test func testAssignToProgressReporterThenSetTotalCount() { + let overall = ProgressManager(totalCount: nil) + + let child1 = ProgressManager(totalCount: 10) + overall.assign(count: 10, to: child1.reporter) + child1.complete(count: 5) + + let child2 = ProgressManager(totalCount: 20) + overall.assign(count: 20, to: child2.reporter) + child2.complete(count: 20) + + overall.setCounts { _, total in + total = 30 + } + #expect(overall.completedCount == 20) + #expect(overall.fractionCompleted == Double(25) / Double(30)) + + child1.complete(count: 5) + + #expect(overall.completedCount == 30) + #expect(overall.fractionCompleted == 1.0) + } + + @Test func testMakeSubprogressThenSetTotalCount() async { + let overall = ProgressManager(totalCount: nil) + + let reporter1 = await dummy(index: 1, subprogress: overall.subprogress(assigningCount: 10)) + + let reporter2 = await dummy(index: 2, subprogress: overall.subprogress(assigningCount: 20)) + + #expect(reporter1.fractionCompleted == 0.5) + + #expect(reporter2.fractionCompleted == 0.5) + + overall.setCounts { _, total in + total = 30 + } + + #expect(overall.totalCount == 30) + #expect(overall.fractionCompleted == 0.5) + } + + func dummy(index: Int, subprogress: consuming Subprogress) async -> ProgressReporter { + let manager = subprogress.start(totalCount: index * 10) + + manager.complete(count: (index * 10) / 2) + + return manager.reporter + } + + #if FOUNDATION_EXIT_TESTS + @Test func testProgressReporterDirectCycleDetection() async { + await #expect(processExitsWith: .failure) { + let manager = ProgressManager(totalCount: 2) + manager.assign(count: 1, to: manager.reporter) + } + } + + @Test func testProgressReporterIndirectCycleDetection() async throws { + await #expect(processExitsWith: .failure) { + let manager = ProgressManager(totalCount: 2) + + let altManager = ProgressManager(totalCount: 1) + altManager.assign(count: 1, to: manager.reporter) + + manager.assign(count: 1, to: altManager.reporter) + } + } + + @Test func testProgressReporterNestedCycleDetection() async throws { + + await #expect(processExitsWith: .failure) { + let manager1 = ProgressManager(totalCount: 1) + + let manager2 = ProgressManager(totalCount: 2) + manager1.assign(count: 1, to: manager2.reporter) + + let manager3 = ProgressManager(totalCount: 3) + manager2.assign(count: 1, to: manager3.reporter) + + manager3.assign(count: 1, to: manager1.reporter) + + } + } + #endif +}