diff --git a/Sources/NIOCore/ByteBuffer-hexdump.swift b/Sources/NIOCore/ByteBuffer-hexdump.swift index fa5dde44e8..b938fe7799 100644 --- a/Sources/NIOCore/ByteBuffer-hexdump.swift +++ b/Sources/NIOCore/ByteBuffer-hexdump.swift @@ -21,6 +21,7 @@ extension ByteBuffer { enum Value: Hashable { case plain(maxBytes: Int? = nil) case detailed(maxBytes: Int? = nil) + case compact(maxBytes: Int? = nil) } let value: Value @@ -32,6 +33,9 @@ extension ByteBuffer { /// A hex dump format compatible with `hexdump` command line utility. public static let detailed = Self(.detailed(maxBytes: nil)) + /// A hex dump analog to `plain` format but without whitespaces. + public static let compact = Self(.compact(maxBytes: nil)) + /// A detailed hex dump format compatible with `xxd`, clipped to `maxBytes` bytes dumped. /// This format will dump first `maxBytes / 2` bytes, and the last `maxBytes / 2` bytes, replacing the rest with " ... ". public static func plain(maxBytes: Int) -> Self { @@ -43,32 +47,50 @@ extension ByteBuffer { public static func detailed(maxBytes: Int) -> Self { Self(.detailed(maxBytes: maxBytes)) } + + /// A hex dump analog to `plain`format but without whitespaces. + /// This format will dump first `maxBytes / 2` bytes, and the last `maxBytes / 2` bytes, with a placeholder in between. + public static func compact(maxBytes: Int) -> Self { + Self(.compact(maxBytes: maxBytes)) + } } - /// Return a `String` of space separated hexadecimal digits of the readable bytes in the buffer, - /// in a format that's compatible with `xxd -r -p`. - /// `hexDumpPlain()` always dumps all readable bytes, i.e. from `readerIndex` to `writerIndex`, - /// so you should set those indices to desired location to get the offset and length that you need to dump. - private func hexDumpPlain() -> String { + /// Shared logic for `hexDumpPlain` and `hexDumpCompact`. + /// Returns a `String` of hexadecimals digits of the readable bytes in the buffer. + /// - parameter + /// - separateWithWhitespace: Controls whether the hex deump will be separated by whitespaces. + private func _hexDump(separateWithWhitespace: Bool) -> String { var hexString = "" - hexString.reserveCapacity(self.readableBytes * 3) + var capacity: Int + + if separateWithWhitespace { + capacity = self.readableBytes * 3 + } else { + capacity = self.readableBytes * 2 + } + + hexString.reserveCapacity(capacity) for byte in self.readableBytesView { hexString += String(byte, radix: 16, padding: 2) - hexString += " " + if separateWithWhitespace { + hexString += " " + } } - return String(hexString.dropLast()) + if separateWithWhitespace { + return String(hexString.dropLast()) + } + + return hexString } - /// Return a `String` of space delimited hexadecimal digits of the readable bytes in the buffer, - /// in a format that's compatible with `xxd -r -p`, but clips the output to the max length of `maxBytes` bytes. - /// If the dump contains more than the `maxBytes` bytes, this function will return the first `maxBytes/2` - /// and the last `maxBytes/2` of that, replacing the rest with `...`, i.e. `01 02 03 ... 09 11 12`. + /// Shared logic for `hexDumpPlain(maxBytes: Int)` and `hexDumpCompact(maxBytes: Int)`. /// /// - parameters: /// - maxBytes: The maximum amount of bytes presented in the dump. - private func hexDumpPlain(maxBytes: Int) -> String { + /// - separateWithWhitespace: Controls whether the dump will be separated by whitespaces. + private func _hexDump(maxBytes: Int, separateWithWhitespace: Bool) -> String { // If the buffer length fits in the max bytes limit in the hex dump, just dump the whole thing. if self.readableBytes <= maxBytes { return self.hexDump(format: .plain) @@ -81,9 +103,56 @@ extension ByteBuffer { buffer.moveReaderIndex(to: buffer.writerIndex - maxBytes / 2) let back = buffer.readSlice(length: buffer.readableBytes)! - let startHex = front.hexDumpPlain() - let endHex = back.hexDumpPlain() - return startHex + " ... " + endHex + let startHex = front._hexDump(separateWithWhitespace: separateWithWhitespace) + let endHex = back._hexDump(separateWithWhitespace: separateWithWhitespace) + + var dots: String + if separateWithWhitespace { + dots = " ... " + } else { + dots = "..." + } + + return startHex + dots + endHex + } + + /// Return a `String` of space separated hexadecimal digits of the readable bytes in the buffer, + /// in a format that's compatible with `xxd -r -p`. + /// `hexDumpPlain()` always dumps all readable bytes, i.e. from `readerIndex` to `writerIndex`, + /// so you should set those indices to desired location to get the offset and length that you need to dump. + private func hexDumpPlain() -> String { + self._hexDump(separateWithWhitespace: true) + } + + /// Return a `String` of space delimited hexadecimal digits of the readable bytes in the buffer, + /// in a format that's compatible with `xxd -r -p`, but clips the output to the max length of `maxBytes` bytes. + /// If the dump contains more than the `maxBytes` bytes, this function will return the first `maxBytes/2` + /// and the last `maxBytes/2` of that, replacing the rest with `...`, i.e. `01 02 03 ... 09 11 12`. + /// + /// - parameters: + /// - maxBytes: The maximum amount of bytes presented in the dump. + private func hexDumpPlain(maxBytes: Int) -> String { + self._hexDump(maxBytes: maxBytes, separateWithWhitespace: true) + } + + /// Return a `String` of hexadecimal digits of the readable bytes in the buffer, + /// analog to `.plain` format but without whitespaces. This format guarantees not to emit whitespaces. + /// `hexDumpCompact()` always dumps all readable bytes, i.e. from `readerIndex` to `writerIndex`, + /// so you should set those indices to desired location to get the offset and length that you need to dump. + private func hexDumpCompact() -> String { + self._hexDump(separateWithWhitespace: false) + } + + /// Return a `String` of hexadecimal digits of the readable bytes in the buffer, + /// analog to `.plain` format but without whitespaces and clips the output to the max length of `maxBytes` bytes. + /// This format guarantees not to emmit whitespaces. + /// If the dump contains more than the `maxBytes` bytes, this function will return the first `maxBytes/2` + /// and the last `maxBytes/2` of that, replacing the rest with `...`, i.e. `010203...091112`. + /// + /// - parameters: + /// - maxBytes: The maximum amount of bytes presented in the dump. + private func hexDumpCompact(maxBytes: Int) -> String { + self._hexDump(maxBytes: maxBytes, separateWithWhitespace: false) } /// Returns a `String` containing a detailed hex dump of this buffer. @@ -240,6 +309,8 @@ extension ByteBuffer { /// `hexDump` provides four formats: /// - `.plain` — plain hex dump format with hex bytes separated by spaces, i.e. `48 65 6c 6c 6f` for `Hello`. This format is compatible with `xxd -r`. /// - `.plain(maxBytes: Int)` — like `.plain`, but clipped to maximum bytes dumped. + /// - `.compact` — plain hexd dump without whitespaces. + /// - `.compact(maxBytes: Int)` — like `.compact`, but clipped to maximum bytes dumped. /// - `.detailed` — detailed hex dump format with both hex, and ASCII representation of the bytes. This format is compatible with what `hexdump -C` outputs. /// - `.detailed(maxBytes: Int)` — like `.detailed`, but clipped to maximum bytes dumped. /// @@ -254,6 +325,13 @@ extension ByteBuffer { return self.hexDumpPlain() } + case .compact(let maxBytes): + if let maxBytes = maxBytes { + return self.hexDumpCompact(maxBytes: maxBytes) + } else { + return self.hexDumpCompact() + } + case .detailed(let maxBytes): if let maxBytes = maxBytes { return self.hexDumpDetailed(maxBytes: maxBytes) diff --git a/Tests/NIOCoreTests/ByteBufferTest.swift b/Tests/NIOCoreTests/ByteBufferTest.swift index 51b64fe1d2..aff6e05178 100644 --- a/Tests/NIOCoreTests/ByteBufferTest.swift +++ b/Tests/NIOCoreTests/ByteBufferTest.swift @@ -1905,6 +1905,33 @@ class ByteBufferTest: XCTestCase { XCTAssertEqual(expected, actual) } + func testHexDumpCompact() { + let buf = ByteBuffer(string: "Hello") + XCTAssertEqual("48656c6c6f", buf.hexDump(format: .compact)) + } + + func testHexDumpCompactEmptyBuffer() { + let buf = ByteBuffer(string: "") + XCTAssertEqual("", buf.hexDump(format: .compact)) + } + + func testHexDumpCompactWithReaderIndexOffset() { + var buf = ByteBuffer(string: "Hello") + let firstTwo = buf.readBytes(length: 2)! + XCTAssertEqual([72, 101], firstTwo) + XCTAssertEqual("6c6c6f", buf.hexDump(format: .compact)) + } + + func testHexDumpCompactWithMaxBytes() { + self.buf.clear() + for f in UInt8.min...UInt8.max { + self.buf.writeInteger(f) + } + let actual = self.buf.hexDump(format: .compact(maxBytes: 10)) + let expected = "0001020304...fbfcfdfeff" + XCTAssertEqual(expected, actual) + } + func testHexDumpDetailed() { let buf = ByteBuffer(string: "Goodbye, world! It was nice knowing you.\n") let expected = """