Skip to content

Commit

Permalink
Add ByteBuffer methods getUTF8ValidatedString and readUTF8ValidatedSt…
Browse files Browse the repository at this point in the history
…ring (#2973)

Add methods to ByteBuffer to read validated UTF8 strings

### Motivation:

The current `readString` and `getString` methods of `ByteBuffer` do not
verify that the string being read is valid UTF8. The Swift 6 standard
library comes with a new initialiser `String(validating:as:)`. This PR
adds alternative methods to ByteBuffer which uses this instead of
`String(decoding:as:)`.

### Modifications:

Added `ByteBuffer.getUTF8ValidatedString(at:length:)`
Added `ByteBuffer.readUTF8ValidatedString(length:)`

### Result:

You can read strings from a ByteBuffer and be certain they are valid
UTF8

---------

Co-authored-by: Cory Benfield <[email protected]>
  • Loading branch information
adam-fowler and Lukasa authored Nov 21, 2024
1 parent 035d3a5 commit 74cf44e
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 0 deletions.
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
}

/// 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

0 comments on commit 74cf44e

Please sign in to comment.