diff --git a/Package.swift b/Package.swift index 3d5543627e..80e340d73d 100644 --- a/Package.swift +++ b/Package.swift @@ -25,11 +25,18 @@ let historicalNIOPosixDependencyRequired: [Platform] = [.macOS, .iOS, .tvOS, .wa let strictConcurrencyDevelopment = false -let strictConcurrencySettings: [SwiftSetting] = { +// Standard Swift settings we apply to all Swift targets +let standardSwiftSettings: [SwiftSetting] = { var initialSettings: [SwiftSetting] = [] initialSettings.append(contentsOf: [ .enableUpcomingFeature("StrictConcurrency"), .enableUpcomingFeature("InferSendableFromCaptures"), + + // The Language Steering Group has promised that they won't break the APIs that currently exist under + // this "experimental" feature flag without two subsequent releases. We assume they will respect that + // promise, so we enable this here. For more, see: + // https://forums.swift.org/t/experimental-support-for-lifetime-dependencies-in-swift-6-2-and-beyond/78638 + .enableExperimentalFeature("Lifetimes"), ]) if strictConcurrencyDevelopment { @@ -83,15 +90,15 @@ let package = Package( swiftCollections, swiftAtomics, ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "_NIODataStructures", - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "_NIOBase64", - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "NIOEmbedded", @@ -102,7 +109,7 @@ let package = Package( swiftAtomics, swiftCollections, ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "NIOPosix", @@ -118,7 +125,7 @@ let package = Package( ], exclude: includePrivacyManifest ? [] : ["PrivacyInfo.xcprivacy"], resources: includePrivacyManifest ? [.copy("PrivacyInfo.xcprivacy")] : [], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "NIO", @@ -127,7 +134,7 @@ let package = Package( "NIOEmbedded", "NIOPosix", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "_NIOConcurrency", @@ -135,7 +142,7 @@ let package = Package( .target(name: "NIO", condition: .when(platforms: historicalNIOPosixDependencyRequired)), "NIOCore", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "NIOFoundationCompat", @@ -143,7 +150,7 @@ let package = Package( .target(name: "NIO", condition: .when(platforms: historicalNIOPosixDependencyRequired)), "NIOCore", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "CNIOAtomics", @@ -190,7 +197,7 @@ let package = Package( dependencies: [ "CNIOAtomics" ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "NIOHTTP1", @@ -201,7 +208,7 @@ let package = Package( "CNIOLLHTTP", swiftCollections, ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "NIOWebSocket", @@ -212,7 +219,7 @@ let package = Package( "CNIOSHA1", "_NIOBase64", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "CNIOLLHTTP", @@ -228,7 +235,7 @@ let package = Package( "NIOCore", swiftCollections, ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "NIOTestUtils", @@ -239,7 +246,7 @@ let package = Package( "NIOHTTP1", swiftAtomics, ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( name: "NIOFileSystem", @@ -255,7 +262,7 @@ let package = Package( path: "Sources/NIOFileSystem", exclude: includePrivacyManifest ? [] : ["PrivacyInfo.xcprivacy"], resources: includePrivacyManifest ? [.copy("PrivacyInfo.xcprivacy")] : [], - swiftSettings: strictConcurrencySettings + [ + swiftSettings: standardSwiftSettings + [ .define("ENABLE_MOCKING", .when(configuration: .debug)) ] ), @@ -266,7 +273,7 @@ let package = Package( "NIOFoundationCompat", ], path: "Sources/NIOFileSystemFoundationCompat", - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .target( @@ -283,7 +290,7 @@ let package = Package( path: "Sources/_NIOFileSystem", exclude: includePrivacyManifest ? [] : ["PrivacyInfo.xcprivacy"], resources: includePrivacyManifest ? [.copy("PrivacyInfo.xcprivacy")] : [], - swiftSettings: strictConcurrencySettings + [ + swiftSettings: standardSwiftSettings + [ .define("ENABLE_MOCKING", .when(configuration: .debug)) ] ), @@ -294,7 +301,7 @@ let package = Package( "NIOFoundationCompat", ], path: "Sources/_NIOFileSystemFoundationCompat", - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), // MARK: - Examples @@ -306,7 +313,7 @@ let package = Package( "NIOCore", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOTCPEchoClient", @@ -315,7 +322,7 @@ let package = Package( "NIOCore", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOEchoServer", @@ -325,7 +332,7 @@ let package = Package( "NIOConcurrencyHelpers", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOEchoClient", @@ -335,7 +342,7 @@ let package = Package( "NIOConcurrencyHelpers", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOHTTP1Server", @@ -346,7 +353,7 @@ let package = Package( "NIOConcurrencyHelpers", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOHTTP1Client", @@ -357,7 +364,7 @@ let package = Package( "NIOConcurrencyHelpers", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOChatServer", @@ -367,7 +374,7 @@ let package = Package( "NIOConcurrencyHelpers", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOChatClient", @@ -377,7 +384,7 @@ let package = Package( "NIOConcurrencyHelpers", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOWebSocketServer", @@ -388,7 +395,7 @@ let package = Package( "NIOWebSocket", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOWebSocketClient", @@ -399,7 +406,7 @@ let package = Package( "NIOWebSocket", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOMulticastChat", @@ -407,7 +414,7 @@ let package = Package( "NIOPosix", "NIOCore", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOUDPEchoServer", @@ -416,7 +423,7 @@ let package = Package( "NIOCore", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOUDPEchoClient", @@ -425,7 +432,7 @@ let package = Package( "NIOCore", ], exclude: ["README.md"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOAsyncAwaitDemo", @@ -434,7 +441,7 @@ let package = Package( "NIOCore", "NIOHTTP1", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), // MARK: - Tests @@ -449,7 +456,7 @@ let package = Package( "NIOFoundationCompat", "NIOWebSocket", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .executableTarget( name: "NIOCrashTester", @@ -461,7 +468,7 @@ let package = Package( "NIOWebSocket", "NIOFoundationCompat", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOCoreTests", @@ -473,7 +480,7 @@ let package = Package( "NIOTestUtils", swiftAtomics, ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOEmbeddedTests", @@ -482,7 +489,7 @@ let package = Package( "NIOCore", "NIOEmbedded", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOPosixTests", @@ -497,7 +504,7 @@ let package = Package( "CNIODarwin", "NIOTLS", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOConcurrencyHelpersTests", @@ -505,17 +512,17 @@ let package = Package( "NIOConcurrencyHelpers", "NIOCore", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIODataStructuresTests", dependencies: ["_NIODataStructures"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOBase64Tests", dependencies: ["_NIOBase64"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOHTTP1Tests", @@ -527,7 +534,7 @@ let package = Package( "NIOFoundationCompat", "NIOTestUtils", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOTLSTests", @@ -538,7 +545,7 @@ let package = Package( "NIOFoundationCompat", "NIOTestUtils", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOWebSocketTests", @@ -547,7 +554,7 @@ let package = Package( "NIOEmbedded", "NIOWebSocket", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOTestUtilsTests", @@ -557,7 +564,7 @@ let package = Package( "NIOEmbedded", "NIOPosix", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOFoundationCompatTests", @@ -565,17 +572,17 @@ let package = Package( "NIOCore", "NIOFoundationCompat", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOTests", dependencies: ["NIO"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOSingletonsTests", dependencies: ["NIOCore", "NIOPosix"], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOFileSystemTests", @@ -586,7 +593,7 @@ let package = Package( swiftCollections, swiftSystem, ], - swiftSettings: strictConcurrencySettings + [ + swiftSettings: standardSwiftSettings + [ .define("ENABLE_MOCKING", .when(configuration: .debug)) ] ), @@ -604,7 +611,7 @@ let package = Package( // the build. "Test Data" ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), .testTarget( name: "NIOFileSystemFoundationCompatTests", @@ -612,7 +619,7 @@ let package = Package( "NIOFileSystem", "NIOFileSystemFoundationCompat", ], - swiftSettings: strictConcurrencySettings + swiftSettings: standardSwiftSettings ), ] ) diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index e95ab8f37d..792b48a273 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -785,6 +785,36 @@ extension ByteBuffer { self = ByteBufferAllocator().buffer(dispatchData: dispatchData) } #endif + + #if compiler(>=6.2) + /// Create a fresh ``ByteBuffer`` with a minimum size, and initializing it safely via an `OutputSpan`. + /// + /// This will allocate a new ``ByteBuffer`` with at least `capacity` bytes of storage, and then calls + /// `initializer` with an `OutputSpan` over the entire allocated storage. This is a convenient method + /// to initialize a buffer directly and safely in a single allocation, including from C code. + /// + /// Once this call returns, the buffer will have its ``ByteBuffer/writerIndex`` appropriately advanced to encompass + /// any memory initialized by the `initializer`. Uninitialized memory will be after the ``ByteBuffer/writerIndex``, + /// available for subsequent use. + /// + /// - info: If you have access to a `Channel`, `ChannelHandlerContext`, or `ByteBufferAllocator` we + /// recommend using `channel.allocator.buffer(capacity:initializingWith:)`. Or if you want to write multiple items into + /// the buffer use `channel.allocator.buffer(capacity: ...)` to allocate a `ByteBuffer` of the right + /// size followed by a `write(minimumWritableBytes:initializingWith:)` instead of using this method. This allows SwiftNIO to do + /// accounting and optimisations of resources acquired for operations on a given `Channel` in the future. + /// + /// - parameters: + /// - capacity: The minimum initial space to allocate for the buffer. + /// - initializer: The initializer that will be invoked to initialize the allocated memory. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public init( + initialCapacity capacity: Int, + initializingWith initializer: (_ span: inout OutputRawSpan) throws -> Void + ) rethrows { + self = try ByteBufferAllocator().buffer(capacity: capacity, initializingWith: initializer) + } + #endif } extension ByteBuffer: Codable { @@ -924,6 +954,32 @@ extension ByteBufferAllocator { return buffer } #endif + + #if compiler(>=6.2) + /// Create a fresh ``ByteBuffer`` with a minimum size, and initializing it safely via an `OutputSpan`. + /// + /// This will allocate a new ``ByteBuffer`` with at least `capacity` bytes of storage, and then calls + /// `initializer` with an `OutputSpan` over the entire allocated storage. This is a convenient method + /// to initialize a buffer directly and safely in a single allocation, including from C code. + /// + /// Once this call returns, the buffer will have its ``ByteBuffer/writerIndex`` appropriately advanced to encompass + /// any memory initialized by the `initializer`. Uninitialized memory will be after the ``ByteBuffer/writerIndex``, + /// available for subsequent use. + /// + /// - parameters: + /// - capacity: The minimum initial space to allocate for the buffer. + /// - initializer: The initializer that will be invoked to initialize the allocated memory. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public func buffer( + capacity: Int, + initializingWith initializer: (_ span: inout OutputRawSpan) throws -> Void + ) rethrows -> ByteBuffer { + var buffer = self.buffer(capacity: capacity) + try buffer.writeWithOutputRawSpan(minimumWritableBytes: capacity, initializingWith: initializer) + return buffer + } + #endif } extension Optional where Wrapped == ByteBuffer { diff --git a/Sources/NIOCore/ByteBuffer-core.swift b/Sources/NIOCore/ByteBuffer-core.swift index add0340b77..e0287fb733 100644 --- a/Sources/NIOCore/ByteBuffer-core.swift +++ b/Sources/NIOCore/ByteBuffer-core.swift @@ -691,6 +691,52 @@ public struct ByteBuffer { return try body(.init(rebasing: self._slicedStorageBuffer[range])) } + #if compiler(>=6.2) + /// Provides safe high-performance read-only access to the readable bytes of this buffer. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public var readableBytesSpan: RawSpan { + @_lifetime(borrow self) + borrowing get { + let range = Range(uncheckedBounds: (lower: self.readerIndex, upper: self.writerIndex)) + return _overrideLifetime(RawSpan(_unsafeBytes: self._slicedStorageBuffer[range]), borrowing: self) + } + } + + /// Provides mutable access to the readable bytes of this buffer. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public var mutableReadableBytesSpan: MutableRawSpan { + @_lifetime(&self) + mutating get { + self._copyStorageAndRebaseIfNeeded() + let range = Range(uncheckedBounds: (lower: self.readerIndex, upper: self.writerIndex)) + return _overrideLifetime(MutableRawSpan(_unsafeBytes: self._slicedStorageBuffer[range]), mutating: &self) + } + } + + /// Enables high-performance low-level appending into the writable section of this buffer. + /// + /// The writer index will be advanced by the number of bytes written into the + /// `OutputRawSpan`. + /// + /// - parameters: + /// - minimumWritableBytes: The minimum initial space to allocate for the buffer. + /// - initializer: The initializer that will be invoked to initialize the allocated memory. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public mutating func writeWithOutputRawSpan( + minimumWritableBytes: Int, + initializingWith initializer: (_ span: inout OutputRawSpan) throws -> Void + ) rethrows { + try self.writeWithUnsafeMutableBytes(minimumWritableBytes: minimumWritableBytes) { ptr in + var span = OutputRawSpan(buffer: ptr, initializedCount: 0) + try initializer(&span) + return span.byteCount + } + } + #endif + /// Yields the bytes currently writable (`bytesWritable` = `capacity` - `writerIndex`). Before reading those bytes you must first /// write to them otherwise you will trigger undefined behaviour. The writer index will remain unchanged. /// diff --git a/Tests/NIOCoreTests/ByteBufferSpanTests.swift b/Tests/NIOCoreTests/ByteBufferSpanTests.swift new file mode 100644 index 0000000000..90a61c4cb3 --- /dev/null +++ b/Tests/NIOCoreTests/ByteBufferSpanTests.swift @@ -0,0 +1,248 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import XCTest + +import NIOCore + +#if compiler(>=6.2) +@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) +final class ByteBufferSpanTests: XCTestCase { + func testReadableBytesSpanOfEmptyByteBuffer() { + let bb = ByteBuffer() + XCTAssertEqual(bb.readableBytesSpan.byteCount, 0) + } + + func testReadableBytesSpanOfSimpleBuffer() { + let bb = ByteBuffer(string: "Hello, world!") + XCTAssertEqual(bb.readableBytesSpan.byteCount, 13) + XCTAssertTrue(bb.readableBytesSpan.elementsEqual("Hello, world!".utf8)) + } + + func testReadableBytesSpanNotAtTheStart() { + var bb = ByteBuffer(string: "Hello, world!") + bb.moveReaderIndex(forwardBy: 5) + XCTAssertEqual(bb.readableBytesSpan.byteCount, 8) + XCTAssertTrue(bb.readableBytesSpan.elementsEqual(", world!".utf8)) + } + + func testReadableBytesSpanOfSlice() { + let first = ByteBuffer(string: "Hello, world!") + let bb = first.getSlice(at: 5, length: 5)! + XCTAssertEqual(bb.readableBytesSpan.byteCount, 5) + XCTAssertTrue(bb.readableBytesSpan.elementsEqual(", wor".utf8)) + } + + func testMutableReadableBytesSpanOfEmptyByteBuffer() { + var bb = ByteBuffer() + XCTAssertEqual(bb.mutableReadableBytesSpan.byteCount, 0) + } + + func testMutableReadableBytesSpanOfSimpleBuffer() { + var bb = ByteBuffer(string: "Hello, world!") + XCTAssertEqual(bb.mutableReadableBytesSpan.byteCount, 13) + XCTAssertTrue(bb.mutableReadableBytesSpan.elementsEqual("Hello, world!".utf8)) + + var readableBytes = bb.mutableReadableBytesSpan + readableBytes.storeBytes(of: UInt8(ascii: "o"), toByteOffset: 5, as: UInt8.self) + + XCTAssertEqual(String(buffer: bb), "Helloo world!") + } + + func testMutableReadableBytesSpanNotAtTheStart() { + var bb = ByteBuffer(string: "Hello, world!") + bb.moveReaderIndex(forwardBy: 5) + XCTAssertEqual(bb.mutableReadableBytesSpan.byteCount, 8) + XCTAssertTrue(bb.mutableReadableBytesSpan.elementsEqual(", world!".utf8)) + + var readableBytes = bb.mutableReadableBytesSpan + readableBytes.storeBytes(of: UInt8(ascii: "o"), toByteOffset: 5, as: UInt8.self) + + XCTAssertEqual(String(buffer: bb), ", worod!") + } + + func testMutableReadableBytesSpanOfSlice() { + let first = ByteBuffer(string: "Hello, world!") + var bb = first.getSlice(at: 5, length: 5)! + XCTAssertEqual(bb.mutableReadableBytesSpan.byteCount, 5) + XCTAssertTrue(bb.mutableReadableBytesSpan.elementsEqual(", wor".utf8)) + + var readableBytes = bb.mutableReadableBytesSpan + readableBytes.storeBytes(of: UInt8(ascii: "o"), toByteOffset: 4, as: UInt8.self) + + XCTAssertEqual(String(buffer: bb), ", woo") + XCTAssertEqual(String(buffer: first), "Hello, world!") + } + + func testEvenCreatingMutableSpanTriggersCoW() { + let first = ByteBuffer(string: "Hello, world!") + var second = first + + let firstBackingPtr = first.withVeryUnsafeBytes { $0 }.baseAddress + let secondBackingPtr = second.withVeryUnsafeBytes { $0 }.baseAddress + XCTAssertEqual(firstBackingPtr, secondBackingPtr) + + let readableBytes = second.mutableReadableBytesSpan + _ = consume readableBytes + let firstNewBackingPtr = first.withVeryUnsafeBytes { $0 }.baseAddress + let secondNewBackingPtr = second.withVeryUnsafeBytes { $0 }.baseAddress + XCTAssertNotEqual(firstNewBackingPtr, secondNewBackingPtr) + XCTAssertEqual(firstBackingPtr, firstNewBackingPtr) + } + + func testAppendingToEmptyBufferViaOutputSpan() { + var bb = ByteBuffer() + bb.writeWithOutputRawSpan(minimumWritableBytes: 15) { span in + XCTAssertEqual(span.byteCount, 0) + XCTAssertGreaterThanOrEqual(span.capacity, 15) + XCTAssertGreaterThanOrEqual(span.freeCapacity, 15) + XCTAssertTrue(span.initializedElementsEqual([])) + + span.append(contentsOf: "Hello, world!".utf8) + + XCTAssertEqual(span.byteCount, 13) + XCTAssertGreaterThanOrEqual(span.capacity, 2) + XCTAssertGreaterThanOrEqual(span.freeCapacity, 2) + XCTAssertTrue(span.initializedElementsEqual("Hello, world!".utf8)) + } + XCTAssertEqual(bb.readableBytes, 13) + XCTAssertEqual(String(buffer: bb), "Hello, world!") + } + + func testAppendingToNonEmptyBufferViaOutputSpanDoesNotExposeInitialBytes() { + var bb = ByteBuffer() + bb.writeString("Hello") + bb.writeWithOutputRawSpan(minimumWritableBytes: 8) { span in + XCTAssertEqual(span.byteCount, 0) + XCTAssertGreaterThanOrEqual(span.capacity, 8) + XCTAssertGreaterThanOrEqual(span.freeCapacity, 8) + XCTAssertTrue(span.initializedElementsEqual([])) + + span.append(contentsOf: ", world!".utf8) + + XCTAssertEqual(span.byteCount, 8) + XCTAssertGreaterThanOrEqual(span.capacity, 0) + XCTAssertGreaterThanOrEqual(span.freeCapacity, 0) + XCTAssertTrue(span.initializedElementsEqual(", world!".utf8)) + } + XCTAssertEqual(bb.readableBytes, 13) + XCTAssertEqual(String(buffer: bb), "Hello, world!") + } + + func testAppendingToASliceViaOutputSpan() { + let first = ByteBuffer(string: "Hello, world!") + var bb = first.getSlice(at: 5, length: 5)! + XCTAssertEqual(bb.mutableReadableBytesSpan.byteCount, 5) + XCTAssertTrue(bb.mutableReadableBytesSpan.elementsEqual(", wor".utf8)) + + bb.writeWithOutputRawSpan(minimumWritableBytes: 5) { span in + span.append(contentsOf: "olleh".utf8) + } + + XCTAssertEqual(String(buffer: bb), ", worolleh") + XCTAssertEqual(String(buffer: first), "Hello, world!") + } + + func testEvenCreatingAnOutputSpanTriggersCoW() { + let first = ByteBuffer(string: "Hello, world!") + var second = first + + let firstBackingPtr = first.withVeryUnsafeBytes { $0 }.baseAddress + let secondBackingPtr = second.withVeryUnsafeBytes { $0 }.baseAddress + XCTAssertEqual(firstBackingPtr, secondBackingPtr) + + second.writeWithOutputRawSpan(minimumWritableBytes: 5) { _ in} + let firstNewBackingPtr = first.withVeryUnsafeBytes { $0 }.baseAddress + let secondNewBackingPtr = second.withVeryUnsafeBytes { $0 }.baseAddress + XCTAssertNotEqual(firstNewBackingPtr, secondNewBackingPtr) + XCTAssertEqual(firstBackingPtr, firstNewBackingPtr) + } + + func testCanCreateEmptyBufferDirectly() { + let bb = ByteBuffer(initialCapacity: 15) { span in + XCTAssertEqual(span.byteCount, 0) + XCTAssertGreaterThanOrEqual(span.capacity, 15) + XCTAssertGreaterThanOrEqual(span.freeCapacity, 15) + XCTAssertTrue(span.initializedElementsEqual([])) + + span.append(contentsOf: "Hello, world!".utf8) + + XCTAssertEqual(span.byteCount, 13) + XCTAssertGreaterThanOrEqual(span.capacity, 2) + XCTAssertGreaterThanOrEqual(span.freeCapacity, 2) + XCTAssertTrue(span.initializedElementsEqual("Hello, world!".utf8)) + } + XCTAssertEqual(bb.readableBytes, 13) + XCTAssertEqual(String(buffer: bb), "Hello, world!") + } + + func testCanCreateEmptyBufferDirectlyFromAllocator() { + let bb = ByteBufferAllocator().buffer(capacity: 15) { span in + XCTAssertEqual(span.byteCount, 0) + XCTAssertGreaterThanOrEqual(span.capacity, 15) + XCTAssertGreaterThanOrEqual(span.freeCapacity, 15) + XCTAssertTrue(span.initializedElementsEqual([])) + + span.append(contentsOf: "Hello, world!".utf8) + + XCTAssertEqual(span.byteCount, 13) + XCTAssertGreaterThanOrEqual(span.capacity, 2) + XCTAssertGreaterThanOrEqual(span.freeCapacity, 2) + XCTAssertTrue(span.initializedElementsEqual("Hello, world!".utf8)) + } + XCTAssertEqual(bb.readableBytes, 13) + XCTAssertEqual(String(buffer: bb), "Hello, world!") + } +} + +@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) +extension RawSpan { + func elementsEqual(_ other: Other) -> Bool where Other.Element == UInt8 { + guard other.count == self.byteCount else { return false } + + var index = other.startIndex + var offset = 0 + while index < other.endIndex { + guard other[index] == self.unsafeLoadUnaligned(fromByteOffset: offset, as: UInt8.self) else { + return false + } + other.formIndex(after: &index) + offset &+= 1 + } + + return true + } +} + +@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) +extension MutableRawSpan { + func elementsEqual(_ other: Other) -> Bool where Other.Element == UInt8 { + self.bytes.elementsEqual(other) + } +} + +@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) +extension OutputRawSpan { + func initializedElementsEqual(_ other: Other) -> Bool where Other.Element == UInt8 { + self.bytes.elementsEqual(other) + } + + @_lifetime(self: copy self) + mutating func append(contentsOf other: Other) where Other.Element == UInt8 { + for element in other { + self.append(element) + } + } +} +#endif