diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index 799b3f8511..5e07f4af2b 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -902,3 +902,66 @@ extension Optional where Wrapped == ByteBuffer { } } } + +#if compiler(>=6) +extension ByteBuffer { + /// Get the string at `index` from this `ByteBuffer` decoding using the UTF-8 encoding. Does not move the reader index. + /// The selected bytes must be readable or else `nil` will be returned. + /// + /// This is an alternative to `ByteBuffer.getString(at:length:)` which ensures the returned string is valid UTF8. If the + /// string is not valid UTF8 then a `ReadUTF8ValidationError` error is thrown. + /// + /// - Parameters: + /// - index: The starting index into `ByteBuffer` containing the string of interest. + /// - length: The number of bytes making up the string. + /// - Returns: A `String` value containing the UTF-8 decoded selected bytes from this `ByteBuffer` or `nil` if + /// the requested bytes are not readable. + @inlinable + @available(macOS 15, iOS 18, tvOS 18, watchOS 11, *) + public func getUTF8ValidatedString(at index: Int, length: Int) throws -> String? { + guard let slice = self.getSlice(at: index, length: length) else { + return nil + } + guard + let string = String( + validating: slice.readableBytesView, + as: Unicode.UTF8.self + ) + else { + throw ReadUTF8ValidationError.invalidUTF8 + } + return string + } + + /// Read `length` bytes off this `ByteBuffer`, decoding it as `String` using the UTF-8 encoding. Move the reader index + /// forward by `length`. + /// + /// This is an alternative to `ByteBuffer.readString(length:)` which ensures the returned string is valid UTF8. If the + /// string is not valid UTF8 then a `ReadUTF8ValidationError` error is thrown and the reader index is not advanced. + /// + /// - Parameters: + /// - length: The number of bytes making up the string. + /// - Returns: A `String` value deserialized from this `ByteBuffer` or `nil` if there aren't at least `length` bytes readable. + @inlinable + @available(macOS 15, iOS 18, tvOS 18, watchOS 11, *) + public mutating func readUTF8ValidatedString(length: Int) throws -> String? { + guard let result = try self.getUTF8ValidatedString(at: self.readerIndex, length: length) else { + return nil + } + self.moveReaderIndex(forwardBy: length) + return result + } + + /// Errors thrown when calling `readUTF8ValidatedString` or `getUTF8ValidatedString`. + public struct ReadUTF8ValidationError: Error, Equatable { + private enum BaseError: Hashable { + case invalidUTF8 + } + + private var baseError: BaseError + + /// The length of the bytes to copy was negative. + public static let invalidUTF8: ReadUTF8ValidationError = .init(baseError: .invalidUTF8) + } +} +#endif // compiler(>=6) diff --git a/Tests/NIOCoreTests/ByteBufferTest.swift b/Tests/NIOCoreTests/ByteBufferTest.swift index 3826b7e1c4..6be0ce37a6 100644 --- a/Tests/NIOCoreTests/ByteBufferTest.swift +++ b/Tests/NIOCoreTests/ByteBufferTest.swift @@ -1315,6 +1315,53 @@ class ByteBufferTest: XCTestCase { XCTAssertEqual("a", buf.readString(length: 1)) } + @available(macOS 15, iOS 18, tvOS 18, watchOS 11, *) + func testReadUTF8ValidatedString() throws { + #if compiler(>=6) + buf.clear() + let expected = "hello" + buf.writeString(expected) + let actual = try buf.readUTF8ValidatedString(length: expected.utf8.count) + XCTAssertEqual(expected, actual) + XCTAssertEqual("", try buf.readUTF8ValidatedString(length: 0)) + XCTAssertNil(try buf.readUTF8ValidatedString(length: 1)) + #else + throw XCTSkip("'readUTF8ValidatedString' is only available in Swift 6 and later") + #endif // compiler(>=6) + } + + @available(macOS 15, iOS 18, tvOS 18, watchOS 11, *) + func testGetUTF8ValidatedString() throws { + #if compiler(>=6) + buf.clear() + let expected = "hello, goodbye" + buf.writeString(expected) + let actual = try buf.getUTF8ValidatedString(at: 7, length: 7) + XCTAssertEqual("goodbye", actual) + #else + throw XCTSkip("'getUTF8ValidatedString' is only available in Swift 6 and later") + #endif // compiler(>=6) + } + + @available(macOS 15, iOS 18, tvOS 18, watchOS 11, *) + func testReadUTF8InvalidString() throws { + #if compiler(>=6) + buf.clear() + buf.writeBytes([UInt8](repeating: 255, count: 16)) + XCTAssertThrowsError(try buf.readUTF8ValidatedString(length: 16)) { error in + switch error { + case is ByteBuffer.ReadUTF8ValidationError: + break + default: + XCTFail("Error: \(error)") + } + } + XCTAssertEqual(buf.readableBytes, 16) + #else + throw XCTSkip("'readUTF8ValidatedString' is only available in Swift 6 and later") + #endif // compiler(>=6) + } + func testSetIntegerBeyondCapacity() throws { var buf = ByteBufferAllocator().buffer(capacity: 32) XCTAssertLessThan(buf.capacity, 200)