Skip to content

Commit

Permalink
Add .compact format to ByteBuffer's hexdump method (#2856)
Browse files Browse the repository at this point in the history
Add compact formatting option to ByteBuffer's hexdump method.

### Motivation:

Resolving the following issue:
#2825.

### Modifications:

• Added a compact format for ByteBuffer's hexdump method

### Result:

A new format which is analog to the `.plain` but without whitespaces.

---------

Co-authored-by: Johannes Weiss <[email protected]>
Co-authored-by: Si Beaumont <[email protected]>
  • Loading branch information
3 people authored Sep 3, 2024
1 parent 30df855 commit 196928d
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 16 deletions.
110 changes: 94 additions & 16 deletions Sources/NIOCore/ByteBuffer-hexdump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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.
///
Expand All @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions Tests/NIOCoreTests/ByteBufferTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand Down

0 comments on commit 196928d

Please sign in to comment.