diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 3281de11a..bebb7b3c7 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -40,7 +40,16 @@ private import UniformTypeIdentifiers @available(_uttypesAPI, *) extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsCGImage { - public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + /// The common implementation of `withUnsafeBytes(for:_:)` and `bytes(for:)`. + /// + /// - Parameters: + /// - attachment: The attachment that is requesting a buffer (that is, the + /// attachment containing this instance.) + /// + /// - Returns: A buffer containing image data representing this value. + /// + /// - Throws: Any error that prevented encoding the image. + private func _data(for attachment: borrowing Attachment<_AttachableImageWrapper>) throws -> NSData { let data = NSMutableData() // Convert the image to a CGImage. @@ -68,14 +77,23 @@ extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: At throw ImageAttachmentError.couldNotConvertImage } + return data + } + + public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { // Pass the bits of the image out to the body. Note that we have an // NSMutableData here so we have to use slightly different API than we would // with an instance of Data. + let data = try _data(for: attachment) return try withExtendedLifetime(data) { try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length)) } } + public borrowing func bytes(for attachment: borrowing Attachment<_AttachableImageWrapper>) throws -> some Collection { + try _data(for: attachment) + } + public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: suggestedName) return (suggestedName as NSString).appendingPathExtension(for: contentType) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift index d82c6a6c6..c4cdb92ad 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift @@ -25,7 +25,12 @@ public import Foundation extension Attachable where Self: Encodable & NSSecureCoding { @_documentation(visibility: private) public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try _Testing_Foundation.withUnsafeBytes(encoding: self, for: attachment, body) + try data(encoding: self, for: attachment).withUnsafeBytes(body) + } + + @_documentation(visibility: private) + public borrowing func bytes(for attachment: borrowing Attachment) throws -> Data { + try data(encoding: self, for: attachment) } } #endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift index 747024de3..ff326815d 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift @@ -10,7 +10,7 @@ #if canImport(Foundation) public import Testing -private import Foundation +internal import Foundation /// A common implementation of ``withUnsafeBytes(for:_:)`` that is used when a /// type conforms to `Encodable`, whether or not it also conforms to @@ -27,7 +27,7 @@ private import Foundation /// /// - Throws: Whatever is thrown by `body`, or any error that prevented the /// creation of the buffer. -func withUnsafeBytes(encoding attachableValue: borrowing E, for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Attachable & Encodable { +func data(encoding attachableValue: borrowing E, for attachment: borrowing Attachment) throws -> Data where E: Attachable & Encodable { let format = try EncodingFormat(for: attachment) let data: Data @@ -47,7 +47,7 @@ func withUnsafeBytes(encoding attachableValue: borrowing E, for attachment data = try JSONEncoder().encode(attachableValue) } - return try data.withUnsafeBytes(body) + return data } // Implement the protocol requirements generically for any encodable value by @@ -96,7 +96,11 @@ extension Attachable where Self: Encodable { /// @Available(Xcode, introduced: 26.0) /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try _Testing_Foundation.withUnsafeBytes(encoding: self, for: attachment, body) + try data(encoding: self, for: attachment).withUnsafeBytes(body) + } + + public borrowing func bytes(for attachment: borrowing Attachment) throws -> some Collection { + try data(encoding: self, for: attachment) } } #endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift index 95002be0a..0f9401df2 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift @@ -21,6 +21,40 @@ public import Foundation /// @Available(Xcode, introduced: 26.0) /// } extension Attachable where Self: NSSecureCoding { + /// The common implementation of `withUnsafeBytes(for:_:)` and `bytes(for:)`. + /// + /// - Parameters: + /// - attachment: The attachment that is requesting a buffer (that is, the + /// attachment containing this instance.) + /// + /// - Returns: A buffer containing property list data representing this value. + /// + /// - Throws: Any error that prevented encoding the value. + private func _data(for attachment: borrowing Attachment) throws -> Data { + let format = try EncodingFormat(for: attachment) + + var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true) + switch format { + case .default: + // The default format is just what NSKeyedArchiver produces. + break + case let .propertyListFormat(propertyListFormat): + // BUG: Foundation does not offer a variant of + // NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:) + // that is Swift-safe (throws errors instead of exceptions) and lets the + // caller specify the output format. Work around this issue by decoding + // the archive re-encoding it manually. + if propertyListFormat != .binary { + let plist = try PropertyListSerialization.propertyList(from: data, format: nil) + data = try PropertyListSerialization.data(fromPropertyList: plist, format: propertyListFormat, options: 0) + } + case .json: + throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "An instance of \(type(of: self)) cannot be encoded as JSON. Specify a property list format instead."]) + } + + return data + } + /// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) /// into a buffer, then call a function and pass that buffer to it. /// @@ -56,28 +90,11 @@ extension Attachable where Self: NSSecureCoding { /// @Available(Xcode, introduced: 26.0) /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - let format = try EncodingFormat(for: attachment) - - var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true) - switch format { - case .default: - // The default format is just what NSKeyedArchiver produces. - break - case let .propertyListFormat(propertyListFormat): - // BUG: Foundation does not offer a variant of - // NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:) - // that is Swift-safe (throws errors instead of exceptions) and lets the - // caller specify the output format. Work around this issue by decoding - // the archive re-encoding it manually. - if propertyListFormat != .binary { - let plist = try PropertyListSerialization.propertyList(from: data, format: nil) - data = try PropertyListSerialization.data(fromPropertyList: plist, format: propertyListFormat, options: 0) - } - case .json: - throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "An instance of \(type(of: self)) cannot be encoded as JSON. Specify a property list format instead."]) - } + try _data(for: attachment).withUnsafeBytes(body) + } - return try data.withUnsafeBytes(body) + public borrowing func bytes(for attachment: borrowing Attachment) throws -> Data { + try _data(for: attachment) } } #endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift index 56f058da3..c1a3a84af 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift @@ -24,5 +24,9 @@ extension Data: Attachable { public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } + + public borrowing func bytes(for attachment: borrowing Attachment) throws -> Data { + copy self + } } #endif diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift index d6be53c80..4d6f55cb5 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift @@ -39,6 +39,10 @@ extension _AttachableURLWrapper: AttachableWrapper { try data.withUnsafeBytes(body) } + public borrowing func bytes(for attachment: borrowing Attachment) throws -> some Collection { + data + } + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { // What extension should we have on the filename so that it has the same // type as the original file (or, in the case of a compressed directory, is diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift index 013e129f6..fc1c5628c 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift @@ -131,6 +131,13 @@ extension ABI.EncodedAttachment: Attachable { #endif } + public borrowing func bytes(for attachment: borrowing Attachment) throws -> some Collection { + if let bytes = _bytes?.rawValue { + return bytes + } + return try withUnsafeBytes(for: attachment) { Array($0) } + } + borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { _preferredName ?? suggestedName } diff --git a/Sources/Testing/Attachments/Attachable.swift b/Sources/Testing/Attachments/Attachable.swift index 8e2c06420..3fbd72a41 100644 --- a/Sources/Testing/Attachments/Attachable.swift +++ b/Sources/Testing/Attachments/Attachable.swift @@ -80,6 +80,10 @@ public protocol Attachable: ~Copyable { /// } borrowing func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R + associatedtype AttachableBytes: Collection = Array + + borrowing func bytes(for attachment: borrowing Attachment) throws -> AttachableBytes + /// Generate a preferred name for the given attachment. /// /// - Parameters: @@ -113,6 +117,12 @@ extension Attachable where Self: ~Copyable { } } +extension Attachable where Self: ~Copyable, Self.AttachableBytes == Array { + public borrowing func bytes(for attachment: borrowing Attachment) throws -> AttachableBytes { + try withUnsafeBytes(for: attachment) { Array($0) } + } +} + extension Attachable where Self: Collection, Element == UInt8 { public var estimatedAttachmentByteCount: Int? { count @@ -141,18 +151,30 @@ extension Array: Attachable { public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } + + public borrowing func bytes(for attachment: borrowing Attachment) throws -> Self { + copy self + } } extension ContiguousArray: Attachable { public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } + + public borrowing func bytes(for attachment: borrowing Attachment) throws -> Self { + copy self + } } extension ArraySlice: Attachable { public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } + + public borrowing func bytes(for attachment: borrowing Attachment) throws -> Self { + copy self + } } extension String: Attachable { @@ -162,6 +184,10 @@ extension String: Attachable { try body(UnsafeRawBufferPointer(utf8)) } } + + public borrowing func bytes(for attachment: borrowing Attachment) throws -> UTF8View { + utf8 + } } extension Substring: Attachable { @@ -171,4 +197,8 @@ extension Substring: Attachable { try body(UnsafeRawBufferPointer(utf8)) } } + + public borrowing func bytes(for attachment: borrowing Attachment) throws -> UTF8View { + utf8 + } } diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 1313b0d41..b0827af87 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -159,6 +159,7 @@ public struct AnyAttachable: AttachableWrapper, Sendable, Copyable { init(_ attachment: Attachment) where A: Attachable & Sendable & ~Copyable { _estimatedAttachmentByteCount = { attachment.attachableValue.estimatedAttachmentByteCount } _withUnsafeBytes = { try attachment.withUnsafeBytes($0) } + _bytes = { try AnyCollection(attachment.bytes) } _preferredName = { attachment.attachableValue.preferredName(for: attachment, basedOn: $0) } } @@ -182,6 +183,12 @@ public struct AnyAttachable: AttachableWrapper, Sendable, Copyable { return result } + private var _bytes: @Sendable () throws -> AnyCollection + + public borrowing func bytes(for attachment: borrowing Attachment) throws -> some Collection { + try _bytes() + } + /// The implementation of ``preferredName(for:basedOn:)`` borrowed from the /// original attachment. private var _preferredName: @Sendable (String) -> String @@ -392,6 +399,12 @@ extension Attachment where AttachableValue: ~Copyable { @inlinable public borrowing func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try attachableValue.withUnsafeBytes(for: self, body) } + + public var bytes: some Collection { + @inlinable get throws { + try attachableValue.bytes(for: self) + } + } } #if !SWT_NO_FILE_IO @@ -492,9 +505,7 @@ extension Attachment where AttachableValue: ~Copyable { // There should be no code path that leads to this call where the attachable // value is nil. - try withUnsafeBytes { buffer in - try file!.write(buffer) - } + try file!.write(bytes) return result } diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index d038db101..45ed7ab32 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -407,7 +407,31 @@ extension FileHandle { let hasContiguousStorage: Void? = try bytes.withContiguousStorageIfAvailable { bytes in try write(bytes, flushAfterward: flushAfterward) } - precondition(hasContiguousStorage != nil, "byte sequence must provide contiguous storage: \(bytes)") + + // (Very) slow path: write one byte at a time. + if hasContiguousStorage == nil { + try withUnsafeCFILEHandle { file in + defer { + if flushAfterward { + fflush(file) + } + } + + try withLock { + for byte in bytes { +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) + if EOF == putc_unlocked(CInt(byte), file) { + throw CError(rawValue: swt_errno()) + } +#else + if EOF == _fputc_nolock(CInt(byte), file) { + throw CError(rawValue: swt_errno()) + } +#endif + } + } + } + } } /// Write a sequence of bytes to this file handle.