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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions Sources/NIOCore/ByteBuffer-aux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -902,3 +902,48 @@ 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
///
/// - 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) -> String? {
guard let range = self.rangeWithinReadableBytes(index: index, length: length) else {
return nil
}
return self.withUnsafeReadableBytes { pointer in
assert(range.lowerBound >= 0 && (range.upperBound - range.lowerBound) <= pointer.count)
return String(validating: UnsafeRawBufferPointer(fastRebase: pointer[range]), as: Unicode.UTF8.self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not the spelling we want. Right now it's not possible to tell if this API call fails because there aren't enough readable bytes or because the string is not valid UTF8. We need to distinguish the two cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could throw an error for invalid strings?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that might be appropriate, yeah.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok it is now throwing an error on invalid UTF8

}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make this method less-scary by using getSlice instead, which will allow us to avoid some of the pointer math.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


/// 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 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) -> String? {
guard let result = self.getUTF8ValidatedString(at: self.readerIndex, length: length) else {
return nil
}
self.moveReaderIndex(forwardBy: length)
return result
}
}
#endif // compiler(>=6)
20 changes: 20 additions & 0 deletions Tests/NIOCoreTests/ByteBufferTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,26 @@ class ByteBufferTest: XCTestCase {
XCTAssertEqual("a", buf.readString(length: 1))
}

@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)
func testReadUTF8ValidatedString() throws {
buf.clear()
let expected = "hello"
buf.writeString(expected)
let actual = buf.readUTF8ValidatedString(length: expected.utf8.count)
XCTAssertEqual(expected, actual)
XCTAssertEqual("", buf.readUTF8ValidatedString(length: 0))
XCTAssertNil(buf.readUTF8ValidatedString(length: 1))
}

@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)
func testReadUTF8InvalidString() throws {
buf.clear()
buf.writeBytes([UInt8](repeating: 255, count: 16))
let actual = buf.readUTF8ValidatedString(length: 16)
XCTAssertNil(actual)
XCTAssertEqual(buf.readableBytes, 16)
}

func testSetIntegerBeyondCapacity() throws {
var buf = ByteBufferAllocator().buffer(capacity: 32)
XCTAssertLessThan(buf.capacity, 200)
Expand Down