From 28cf942f743c2ec0f51ccd681fa4c754beb7d4a1 Mon Sep 17 00:00:00 2001 From: Ali Ali Date: Tue, 6 Aug 2024 21:58:55 +1000 Subject: [PATCH] Issue-2748 - Add ByteBuffer Hex init & write --- Sources/NIOCore/ByteBuffer-aux.swift | 18 ++++++++ Sources/NIOCore/ByteBuffer-conversions.swift | 2 +- ...fer-hexdump.swift => ByteBuffer-hex.swift} | 42 +++++++++++++++++++ Tests/NIOCoreTests/ByteBufferTest.swift | 34 +++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) rename Sources/NIOCore/{ByteBuffer-hexdump.swift => ByteBuffer-hex.swift} (85%) diff --git a/Sources/NIOCore/ByteBuffer-aux.swift b/Sources/NIOCore/ByteBuffer-aux.swift index fcfa74ce3b..765e03468f 100644 --- a/Sources/NIOCore/ByteBuffer-aux.swift +++ b/Sources/NIOCore/ByteBuffer-aux.swift @@ -88,6 +88,24 @@ extension ByteBuffer { ) } + // MARK: Hex encoded string APIs + /// Write `hex encoded string` into this `ByteBuffer`, moving the writer index forward appropriately. + /// + /// - parameters: + /// - string: The hex encoded string to write. + /// - returns: The number of bytes written. + @discardableResult + @inlinable + public mutating func writeHexEncodedBytes(_ string: String) -> Int { + let hexPlainDecodedBytes = string.hexPlainDecodedBytes + guard !hexPlainDecodedBytes.isEmpty else { + return 0 + } + let written = self.setBytes(hexPlainDecodedBytes, at: self.writerIndex) + self._moveWriterIndex(forwardBy: written) + return written + } + // MARK: String APIs /// Write `string` into this `ByteBuffer` using UTF-8 encoding, moving the writer index forward appropriately. /// diff --git a/Sources/NIOCore/ByteBuffer-conversions.swift b/Sources/NIOCore/ByteBuffer-conversions.swift index d1f4f0eef3..77d52d563b 100644 --- a/Sources/NIOCore/ByteBuffer-conversions.swift +++ b/Sources/NIOCore/ByteBuffer-conversions.swift @@ -40,7 +40,7 @@ extension String { /// /// - parameters: /// - radix: radix base to use for conversion. - /// - padding: the desired lenght of the resulting string. + /// - padding: the desired length of the resulting string. @inlinable internal init(_ value: Value, radix: Int, padding: Int) where Value: BinaryInteger { let formatted = String(value, radix: radix) diff --git a/Sources/NIOCore/ByteBuffer-hexdump.swift b/Sources/NIOCore/ByteBuffer-hex.swift similarity index 85% rename from Sources/NIOCore/ByteBuffer-hexdump.swift rename to Sources/NIOCore/ByteBuffer-hex.swift index fa5dde44e8..5be50916a4 100644 --- a/Sources/NIOCore/ByteBuffer-hexdump.swift +++ b/Sources/NIOCore/ByteBuffer-hex.swift @@ -12,8 +12,30 @@ // //===----------------------------------------------------------------------===// +import Foundation + extension ByteBuffer { + /// Create a fresh `ByteBuffer` containing the `bytes` decoded from the string representation of `hexEncodedBytes`. + /// + /// This will allocate a new `ByteBuffer` with enough space to fit the hex decoded `bytes` and potentially some extra space + /// using the default allocator. + /// + /// - info: If you have access to a `Channel`, `ChannelHandlerContext`, or `ByteBufferAllocator` we + /// recommend using `channel.allocator.buffer(integer:)`. Or if you want to write multiple items into the + /// buffer use `channel.allocator.buffer(capacity: ...)` to allocate a `ByteBuffer` of the right + /// size followed by a `writeHexEncodedBytes` instead of using this method. This allows SwiftNIO to do + /// accounting and optimisations of resources acquired for operations on a given `Channel` in the future. + init(hexEncodedBytes string: String) { + let hexPlainDecodedBytes = string.hexPlainDecodedBytes + guard !hexPlainDecodedBytes.isEmpty else { + self = ByteBufferAllocator.zeroCapacityWithDefaultAllocator + return + } + + self = ByteBufferAllocator().buffer(bytes: hexPlainDecodedBytes) + } + /// Describes a ByteBuffer hexDump format. /// Can be either xxd output compatible, or hexdump compatible. public struct HexDumpFormat: Hashable, Sendable { @@ -263,3 +285,23 @@ extension ByteBuffer { } } } + +extension String { + /// Plain decode a string representing a hexadecimal sequence them into an UInt8 sequence + /// - Complexity: O(n) + public var hexPlainDecodedBytes: [UInt8] { + let stringWithoutWhiteSpaces = self.replacingOccurrences(of: " ", with: "") + return sequence( + state: stringWithoutWhiteSpaces, + next: { remainder in + guard remainder.count >= 2 else { + precondition(remainder.count == 0, "Uneven number of hex encoded bytes within string") + return nil + } + let nextTwo = remainder.prefix(2) + remainder.removeFirst(2) + return UInt8(nextTwo, radix: 16) + } + ).map { $0 } + } +} diff --git a/Tests/NIOCoreTests/ByteBufferTest.swift b/Tests/NIOCoreTests/ByteBufferTest.swift index 51b64fe1d2..4a0a4bfdf9 100644 --- a/Tests/NIOCoreTests/ByteBufferTest.swift +++ b/Tests/NIOCoreTests/ByteBufferTest.swift @@ -1069,6 +1069,17 @@ class ByteBufferTest: XCTestCase { XCTAssertNil(self.buf.getString(at: 0, length: capacity + 1)) } + func testWriteEmptyByteArray() throws { + var buffer = ByteBufferAllocator().buffer(capacity: 32) + buffer.moveWriterIndex(to: 16) + buffer.moveReaderIndex(to: 16) + XCTAssertEqual(buffer.setBytes([], at: 16), 0) + XCTAssertEqual(buffer.readableBytes, 0) + XCTAssertEqual(buffer.writableBytes, 16) + XCTAssertEqual(buffer.writerIndex, 16) + XCTAssertEqual(buffer.readerIndex, 16) + } + func testSetGetBytesAllFine() throws { self.buf.moveReaderIndex(to: 0) self.buf.setBytes([1, 2, 3, 4], at: 0) @@ -1878,6 +1889,29 @@ class ByteBufferTest: XCTestCase { XCTAssertEqual(expected, actual) } + func testWriteHexEncodedBytes() throws { + var buffer = ByteBuffer(hexEncodedBytes: "68 65 6c 6c 6f 20 77 6f 72 6c 64 0a") + XCTAssertEqual(buffer.writeHexEncodedBytes("68656c6c6f20776f726c64"), 11) + XCTAssertEqual(buffer.writeHexEncodedBytes(" 0a "), 1) + XCTAssertEqual(buffer.writeHexEncodedBytes(""), 0) + XCTAssertEqual(buffer.writeHexEncodedBytes(" "), 0) + XCTAssertEqual(ByteBuffer(string: "hello world\nhello world\n"), buffer) + } + + func testHexInitialiser() { + var allBytes = ByteBufferAllocator().buffer(capacity: Int(UInt8.max)) + for x in UInt8.min...UInt8.max { + allBytes.writeInteger(x) + } + + let allBytesHex = allBytes.hexDump(format: .plain) + let allBytesDecoded = ByteBuffer(hexEncodedBytes: allBytesHex) + XCTAssertEqual(allBytes, allBytesDecoded) + + // Edge case + XCTAssertEqual(ByteBuffer(hexEncodedBytes: " "), ByteBufferAllocator.zeroCapacityWithDefaultAllocator) + } + func testHexDumpPlain() { let buf = ByteBuffer(string: "Hello") XCTAssertEqual("48 65 6c 6c 6f", buf.hexDump(format: .plain))