Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ByteBuffer methods getUTF8ValidatedString and readUTF8ValidatedString #2973

Merged
merged 14 commits into from
Nov 21, 2024
Merged
63 changes: 63 additions & 0 deletions Sources/NIOCore/ByteBuffer-aux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
adam-fowler marked this conversation as resolved.
Show resolved Hide resolved

/// 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)
47 changes: 47 additions & 0 deletions Tests/NIOCoreTests/ByteBufferTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading