From eeaaf3efaa1d91da208b97f5e3e97ac402908363 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Sat, 24 May 2025 15:34:44 +0330 Subject: [PATCH 01/10] Add read `InlineArray` + tests --- Sources/NIOCore/ByteBuffer-aux.swift | 42 +++++++++++++++++++++++++ Tests/NIOCoreTests/ByteBufferTest.swift | 41 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index 5555941cb6..c877965aa7 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -58,6 +58,48 @@ extension ByteBuffer { return result } + #if compiler(>=6.2) && !canImport(Darwin) + @inlinable + public mutating func readInlineArray< + let count: Int, + IntegerType: FixedWidthInteger + >( + endianness: Endianness = .big, + as: InlineArray.Type = InlineArray.self + ) -> InlineArray? { + let length = MemoryLayout.size + let bytesRequired = length &* count + + guard self.readableBytes >= bytesRequired else { + return nil + } + + return self.readWithUnsafeReadableBytes { + ptr -> (Int, InlineArray) in + assert(ptr.count >= bytesRequired) + let values: InlineArray = InlineArray { index in + switch endianness { + case .big: + return IntegerType( + bigEndian: ptr.load( + fromByteOffset: index &* length, + as: IntegerType.self + ) + ) + case .little: + return IntegerType( + littleEndian: ptr.load( + fromByteOffset: index &* length, + as: IntegerType.self + ) + ) + } + } + return (bytesRequired, values) + } + } + #endif + /// Returns the Bytes at the current reader index without advancing it. /// /// This method is equivalent to calling `getBytes(at: readerIndex, ...)` diff --git a/Tests/NIOCoreTests/ByteBufferTest.swift b/Tests/NIOCoreTests/ByteBufferTest.swift index 24e310c2ec..5f34313ded 100644 --- a/Tests/NIOCoreTests/ByteBufferTest.swift +++ b/Tests/NIOCoreTests/ByteBufferTest.swift @@ -3932,6 +3932,47 @@ extension ByteBufferTest { XCTAssertEqual(0, self.buf.readableBytes, "Buffer should be fully consumed after all reads.") } + #if compiler(>=6.2) && !canImport(Darwin) + func testReadInlineArrayOfUInt8() throws { + let bytes = (0..<10).map { _ in UInt8.random(in: .min ... .max) } + + let startWriterIndex = self.buf.writerIndex + let written = self.buf.writeBytes(bytes) + XCTAssertEqual(startWriterIndex + written, self.buf.writerIndex) + XCTAssertEqual(written, self.buf.readableBytes) + + let result = try XCTUnwrap( + self.buf.readInlineArray(as: InlineArray<10, UInt8>.self) + ) + XCTAssertEqual(10, result.count) + for idx in result.indices { + XCTAssertEqual(bytes[idx], result[idx]) + } + XCTAssertEqual(0, self.buf.readableBytes) + } + + func testReadInlineArrayOfUInt64() throws { + let bytes = (0..<10).map { _ in UInt64.random(in: .min ... .max) } + + let startWriterIndex = self.buf.writerIndex + var written = 0 + for byte in bytes { + written += self.buf.writeInteger(byte) + } + XCTAssertEqual(startWriterIndex + written, self.buf.writerIndex) + XCTAssertEqual(written, self.buf.readableBytes) + + let result = try XCTUnwrap( + self.buf.readInlineArray(as: InlineArray<10, UInt64>.self) + ) + XCTAssertEqual(10, result.count) + for idx in result.indices { + XCTAssertEqual(bytes[idx], result[idx]) + } + XCTAssertEqual(0, self.buf.readableBytes) + } + #endif + func testByteBufferEncode() throws { let encoder = JSONEncoder() let hello = "Hello, world!" From a054b6174c7da4cc65125116b9f873555b9dc565 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Fri, 30 May 2025 22:22:14 +0330 Subject: [PATCH 02/10] use `getInteger(at:)` --- Sources/NIOCore/ByteBuffer-aux.swift | 43 +++++++++++++++------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index c877965aa7..4e1435ff3e 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -68,34 +68,31 @@ extension ByteBuffer { as: InlineArray.Type = InlineArray.self ) -> InlineArray? { let length = MemoryLayout.size - let bytesRequired = length &* count + let bytesRequired = length * count guard self.readableBytes >= bytesRequired else { return nil } - return self.readWithUnsafeReadableBytes { - ptr -> (Int, InlineArray) in - assert(ptr.count >= bytesRequired) - let values: InlineArray = InlineArray { index in - switch endianness { - case .big: - return IntegerType( - bigEndian: ptr.load( - fromByteOffset: index &* length, - as: IntegerType.self - ) - ) - case .little: - return IntegerType( - littleEndian: ptr.load( - fromByteOffset: index &* length, - as: IntegerType.self - ) + do { + let inlineArray = try InlineArray { index in + guard + let integer = self.getInteger( + at: index * length, + endianness: endianness, + as: IntegerType.self ) + else { + throw InlineArrayFailedToGetElementError() } + return integer } - return (bytesRequired, values) + // Already made sure of 'self.readableBytes >= bytesRequired' above + self._moveReaderIndex(forwardBy: bytesRequired) + return inlineArray + } catch { + // Only InlineArrayFailedToGetElementError could have been thrown + return nil } } #endif @@ -1067,3 +1064,9 @@ extension ByteBuffer { } } #endif // compiler(>=6) + +@usableFromInline +struct InlineArrayFailedToGetElementError: Error { + @usableFromInline + init() {} +} From 7cb4052cbb6c0aa1cd14a7928769355a1306e991 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Sat, 31 May 2025 18:22:24 +0330 Subject: [PATCH 03/10] use @_spi(InlineArray) + don't user !Darwin + better comments --- Sources/NIOCore/ByteBuffer-aux.swift | 8 +++++--- Tests/NIOCoreTests/ByteBufferTest.swift | 12 +++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index 4e1435ff3e..d8cf5e8851 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -58,7 +58,8 @@ extension ByteBuffer { return result } - #if compiler(>=6.2) && !canImport(Darwin) + #if compiler(>=6.2) + @_spi(InlineArray) @inlinable public mutating func readInlineArray< let count: Int, @@ -87,11 +88,12 @@ extension ByteBuffer { } return integer } - // Already made sure of 'self.readableBytes >= bytesRequired' above + // Already made sure of 'self.readableBytes >= bytesRequired' above, also + // 'getInteger()'s have succeeded by this point which have their own bounds checks self._moveReaderIndex(forwardBy: bytesRequired) return inlineArray } catch { - // Only InlineArrayFailedToGetElementError could have been thrown + // Only 'InlineArrayFailedToGetElementError' could have been thrown return nil } } diff --git a/Tests/NIOCoreTests/ByteBufferTest.swift b/Tests/NIOCoreTests/ByteBufferTest.swift index 5f34313ded..5da7acc520 100644 --- a/Tests/NIOCoreTests/ByteBufferTest.swift +++ b/Tests/NIOCoreTests/ByteBufferTest.swift @@ -19,7 +19,7 @@ import _NIOBase64 import struct Foundation.Data -@testable import NIOCore +@testable @_spi(InlineArray) import NIOCore class ByteBufferTest: XCTestCase { private let allocator = ByteBufferAllocator() @@ -3932,8 +3932,8 @@ extension ByteBufferTest { XCTAssertEqual(0, self.buf.readableBytes, "Buffer should be fully consumed after all reads.") } - #if compiler(>=6.2) && !canImport(Darwin) func testReadInlineArrayOfUInt8() throws { + #if compiler(>=6.2) let bytes = (0..<10).map { _ in UInt8.random(in: .min ... .max) } let startWriterIndex = self.buf.writerIndex @@ -3949,9 +3949,13 @@ extension ByteBufferTest { XCTAssertEqual(bytes[idx], result[idx]) } XCTAssertEqual(0, self.buf.readableBytes) + #else + throw XCTSkip("'InlineArray' is only available in Swift 6.2 and later") + #endif // compiler(>=6.2) } func testReadInlineArrayOfUInt64() throws { + #if compiler(>=6.2) let bytes = (0..<10).map { _ in UInt64.random(in: .min ... .max) } let startWriterIndex = self.buf.writerIndex @@ -3970,8 +3974,10 @@ extension ByteBufferTest { XCTAssertEqual(bytes[idx], result[idx]) } XCTAssertEqual(0, self.buf.readableBytes) + #else + throw XCTSkip("'InlineArray' is only available in Swift 6.2 and later") + #endif // compiler(>=6.2) } - #endif func testByteBufferEncode() throws { let encoder = JSONEncoder() From 7470c9fe5f5958f2d5d71a00e2b2aff49f4e030a Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Sat, 31 May 2025 20:48:11 +0330 Subject: [PATCH 04/10] some comments --- Sources/NIOCore/ByteBuffer-aux.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index d8cf5e8851..81f5f63a93 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -88,12 +88,11 @@ extension ByteBuffer { } return integer } - // Already made sure of 'self.readableBytes >= bytesRequired' above, also - // 'getInteger()'s have succeeded by this point which have their own bounds checks + // already made sure of 'self.readableBytes >= bytesRequired' above self._moveReaderIndex(forwardBy: bytesRequired) return inlineArray } catch { - // Only 'InlineArrayFailedToGetElementError' could have been thrown + // only 'InlineArrayFailedToGetElementError' could have been thrown return nil } } From 4ab850e78a449d21977723c3cff831ec33013ee7 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Sat, 31 May 2025 20:48:35 +0330 Subject: [PATCH 05/10] use `stride` to account for padding bytes --- Sources/NIOCore/ByteBuffer-aux.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index 81f5f63a93..c77b83b42c 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -68,8 +68,9 @@ extension ByteBuffer { endianness: Endianness = .big, as: InlineArray.Type = InlineArray.self ) -> InlineArray? { - let length = MemoryLayout.size - let bytesRequired = length * count + // use stride to account for padding bytes + let stride = MemoryLayout.stride + let bytesRequired = stride * count guard self.readableBytes >= bytesRequired else { return nil @@ -79,7 +80,8 @@ extension ByteBuffer { let inlineArray = try InlineArray { index in guard let integer = self.getInteger( - at: index * length, + // this is less than 'bytesRequired' so is safe to multiply + at: stride &* index, endianness: endianness, as: IntegerType.self ) From aa7ac7c8e1e76e05e92d53f2e83feaaaeffbd3d5 Mon Sep 17 00:00:00 2001 From: Mahdi Bahrami Date: Wed, 25 Jun 2025 02:26:38 +0330 Subject: [PATCH 06/10] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ebec19db3..6e4f026c88 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: push: branches: [main] schedule: - - cron: "0 8,20 * * *" + # - cron: "0 8,20 * * *" jobs: unit-tests: From 00071f1625acfd263c1a2652f2964b9a049609ea Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Sun, 21 Sep 2025 23:50:08 +0330 Subject: [PATCH 07/10] Update availability checks, move the tests to the 6.2-guarded area --- .github/workflows/main.yml | 2 +- Sources/NIOCore/ByteBuffer-aux.swift | 2 +- Tests/NIOCoreTests/ByteBufferTest.swift | 92 ++++++++++++------------- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fcd74f62bd..5178bcce81 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: push: branches: [main] schedule: - # - cron: "0 8,20 * * *" + - cron: "0 8,20 * * *" jobs: unit-tests: diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index 78ba2e3103..a8ca8947f5 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -59,8 +59,8 @@ extension ByteBuffer { } #if compiler(>=6.2) - @_spi(InlineArray) @inlinable + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) public mutating func readInlineArray< let count: Int, IntegerType: FixedWidthInteger diff --git a/Tests/NIOCoreTests/ByteBufferTest.swift b/Tests/NIOCoreTests/ByteBufferTest.swift index e44110352e..7f4ad77167 100644 --- a/Tests/NIOCoreTests/ByteBufferTest.swift +++ b/Tests/NIOCoreTests/ByteBufferTest.swift @@ -19,7 +19,7 @@ import _NIOBase64 import struct Foundation.Data -@testable @_spi(InlineArray) import NIOCore +@testable import NIOCore class ByteBufferTest: XCTestCase { private let allocator = ByteBufferAllocator() @@ -3932,53 +3932,6 @@ extension ByteBufferTest { XCTAssertEqual(0, self.buf.readableBytes, "Buffer should be fully consumed after all reads.") } - func testReadInlineArrayOfUInt8() throws { - #if compiler(>=6.2) - let bytes = (0..<10).map { _ in UInt8.random(in: .min ... .max) } - - let startWriterIndex = self.buf.writerIndex - let written = self.buf.writeBytes(bytes) - XCTAssertEqual(startWriterIndex + written, self.buf.writerIndex) - XCTAssertEqual(written, self.buf.readableBytes) - - let result = try XCTUnwrap( - self.buf.readInlineArray(as: InlineArray<10, UInt8>.self) - ) - XCTAssertEqual(10, result.count) - for idx in result.indices { - XCTAssertEqual(bytes[idx], result[idx]) - } - XCTAssertEqual(0, self.buf.readableBytes) - #else - throw XCTSkip("'InlineArray' is only available in Swift 6.2 and later") - #endif // compiler(>=6.2) - } - - func testReadInlineArrayOfUInt64() throws { - #if compiler(>=6.2) - let bytes = (0..<10).map { _ in UInt64.random(in: .min ... .max) } - - let startWriterIndex = self.buf.writerIndex - var written = 0 - for byte in bytes { - written += self.buf.writeInteger(byte) - } - XCTAssertEqual(startWriterIndex + written, self.buf.writerIndex) - XCTAssertEqual(written, self.buf.readableBytes) - - let result = try XCTUnwrap( - self.buf.readInlineArray(as: InlineArray<10, UInt64>.self) - ) - XCTAssertEqual(10, result.count) - for idx in result.indices { - XCTAssertEqual(bytes[idx], result[idx]) - } - XCTAssertEqual(0, self.buf.readableBytes) - #else - throw XCTSkip("'InlineArray' is only available in Swift 6.2 and later") - #endif // compiler(>=6.2) - } - func testByteBufferEncode() throws { let encoder = JSONEncoder() let hello = "Hello, world!" @@ -4508,5 +4461,48 @@ extension ByteBufferTest { let result = self.buf.readBytes(length: 4) XCTAssertEqual(Array(0..<4), result!) } + + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) + func testReadInlineArrayOfUInt8() throws { + let bytes = (0..<10).map { _ in UInt8.random(in: .min ... .max) } + + let startWriterIndex = self.buf.writerIndex + let written = self.buf.writeBytes(bytes) + XCTAssertEqual(startWriterIndex + written, self.buf.writerIndex) + XCTAssertEqual(written, self.buf.readableBytes) + + let result = try XCTUnwrap( + self.buf.readInlineArray(as: InlineArray<10, UInt8>.self) + ) + XCTAssertEqual(10, result.count) + for idx in result.indices { + XCTAssertEqual(bytes[idx], result[idx]) + } + XCTAssertEqual(0, self.buf.readableBytes) + XCTAssertEqual(10, self.buf.readerIndex) + } + + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) + func testReadInlineArrayOfUInt64() throws { + let bytes = (0..<10).map { _ in UInt64.random(in: .min ... .max) } + + let startWriterIndex = self.buf.writerIndex + var written = 0 + for byte in bytes { + written += self.buf.writeInteger(byte) + } + XCTAssertEqual(startWriterIndex + written, self.buf.writerIndex) + XCTAssertEqual(written, self.buf.readableBytes) + + let result = try XCTUnwrap( + self.buf.readInlineArray(as: InlineArray<10, UInt64>.self) + ) + XCTAssertEqual(10, result.count) + for idx in result.indices { + XCTAssertEqual(bytes[idx], result[idx]) + } + XCTAssertEqual(0, self.buf.readableBytes) + XCTAssertEqual(80, self.buf.readerIndex) + } } #endif From 10e8e0ccefd4505e6cb1749dcf7a404f68271be9 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Sun, 21 Sep 2025 23:56:28 +0330 Subject: [PATCH 08/10] Use `InlineArray`'s `inout OutputSpan` init --- Sources/NIOCore/ByteBuffer-aux.swift | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index a8ca8947f5..f8e44b8cc6 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -77,18 +77,20 @@ extension ByteBuffer { } do { - let inlineArray = try InlineArray { index in - guard - let integer = self.getInteger( - // this is less than 'bytesRequired' so is safe to multiply - at: stride &* index, - endianness: endianness, - as: IntegerType.self - ) - else { - throw InlineArrayFailedToGetElementError() + let inlineArray = try InlineArray { (outputSpan: inout OutputSpan) in + for index in 0..= bytesRequired' above self._moveReaderIndex(forwardBy: bytesRequired) From 17bdfc24099c1a3cb8506b587c87db3c898eade2 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Mon, 22 Sep 2025 11:33:12 +0330 Subject: [PATCH 09/10] Force-unwrap get-integer --- Sources/NIOCore/ByteBuffer-aux.swift | 41 ++++++++++------------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index f8e44b8cc6..39db01bc4f 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -76,29 +76,22 @@ extension ByteBuffer { return nil } - do { - let inlineArray = try InlineArray { (outputSpan: inout OutputSpan) in - for index in 0.. { (outputSpan: inout OutputSpan) in + for index in 0..= bytesRequired' above, + // so this is safe to force-unwrap as it's guaranteed to exist + let integer = self.getInteger( + // this is less than 'bytesRequired' so is safe to multiply + at: stride &* index, + endianness: endianness, + as: IntegerType.self + )! + outputSpan.append(integer) } - // already made sure of 'self.readableBytes >= bytesRequired' above - self._moveReaderIndex(forwardBy: bytesRequired) - return inlineArray - } catch { - // only 'InlineArrayFailedToGetElementError' could have been thrown - return nil } + // already made sure of 'self.readableBytes >= bytesRequired' above + self._moveReaderIndex(forwardBy: bytesRequired) + return inlineArray } #endif @@ -1085,9 +1078,3 @@ extension ByteBuffer { } } #endif // compiler(>=6) - -@usableFromInline -struct InlineArrayFailedToGetElementError: Error { - @usableFromInline - init() {} -} From 9566a5e6ab56eaed5148a3cf2712e486b730b746 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Mon, 22 Sep 2025 11:41:49 +0330 Subject: [PATCH 10/10] Add a test + minor modification to another test --- Tests/NIOCoreTests/ByteBufferTest.swift | 26 +++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/Tests/NIOCoreTests/ByteBufferTest.swift b/Tests/NIOCoreTests/ByteBufferTest.swift index 7f4ad77167..7885d463ce 100644 --- a/Tests/NIOCoreTests/ByteBufferTest.swift +++ b/Tests/NIOCoreTests/ByteBufferTest.swift @@ -4484,7 +4484,7 @@ extension ByteBufferTest { @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) func testReadInlineArrayOfUInt64() throws { - let bytes = (0..<10).map { _ in UInt64.random(in: .min ... .max) } + let bytes = (0..<15).map { _ in UInt64.random(in: .min ... .max) } let startWriterIndex = self.buf.writerIndex var written = 0 @@ -4495,14 +4495,32 @@ extension ByteBufferTest { XCTAssertEqual(written, self.buf.readableBytes) let result = try XCTUnwrap( - self.buf.readInlineArray(as: InlineArray<10, UInt64>.self) + self.buf.readInlineArray(as: InlineArray<15, UInt64>.self) ) - XCTAssertEqual(10, result.count) + XCTAssertEqual(15, result.count) for idx in result.indices { XCTAssertEqual(bytes[idx], result[idx]) } XCTAssertEqual(0, self.buf.readableBytes) - XCTAssertEqual(80, self.buf.readerIndex) + XCTAssertEqual(120, self.buf.readerIndex) + } + + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) + func testNotEnoughBytesToReadInlineArrayOfInt32() throws { + let startWriterIndex = self.buf.writerIndex + var written = 0 + /// Write 15 bytes. This won't be enough to read an `InlineArray<5, Int32>`. + for _ in 0..<15 { + written += self.buf.writeInteger(UInt8.random(in: .min ... .max)) + } + XCTAssertEqual(startWriterIndex + written, self.buf.writerIndex) + XCTAssertEqual(written, self.buf.readableBytes) + + let result = self.buf.readInlineArray(as: InlineArray<5, Int32>.self) + + XCTAssertNil(result) + XCTAssertEqual(written, self.buf.readableBytes) + XCTAssertEqual(0, self.buf.readerIndex) } } #endif