Skip to content

Commit

Permalink
Add .compact format to ByteBuffer's hexdump method
Browse files Browse the repository at this point in the history
Motivation:

Resolving the following issue: apple#2825.

Modifications:

• Added a compact format for ByteBuffer's hexdump method

Result:

A new format which is analog to the `.plain` but without whitespaces.
  • Loading branch information
supersonicbyte committed Sep 1, 2024
1 parent 76f39c1 commit b0602d2
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 0 deletions.
59 changes: 59 additions & 0 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 @@ -31,6 +32,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 " ... ".
Expand All @@ -43,6 +47,12 @@ 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,
Expand Down Expand Up @@ -85,6 +95,46 @@ extension ByteBuffer {
let endHex = back.hexDumpPlain()
return startHex + " ... " + endHex
}

/// Return a `String` of hexadecimal digits of the readable bytes in the buffer,
/// analog to `.plain` format but without 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 {
var hexString = ""
hexString.reserveCapacity(self.readableBytes * 3)

for byte in self.readableBytesView {
hexString += String(byte, radix: 16, padding: 2)
}

return hexString
}

/// 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.
/// 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 hexDumpCompact(maxBytes: Int) -> 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)
}

var buffer = self

// Safe to force-unwrap because we just checked readableBytes is > maxBytes above.
let front = buffer.readSlice(length: maxBytes / 2)!
buffer.moveReaderIndex(to: buffer.writerIndex - maxBytes / 2)
let back = buffer.readSlice(length: buffer.readableBytes)!

let startHex = front.hexDumpCompact()
let endHex = back.hexDumpCompact()
return startHex + " ... " + endHex
}

/// Returns a `String` containing a detailed hex dump of this buffer.
/// Intended to be used internally in ``hexDump(format:)``
Expand Down Expand Up @@ -240,6 +290,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 @@ -253,6 +305,13 @@ extension ByteBuffer {
} else {
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 {
Expand Down
27 changes: 27 additions & 0 deletions Tests/NIOCoreTests/ByteBufferTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1904,6 +1904,33 @@ class ByteBufferTest: XCTestCase {
let expected = "00 01 02 03 04 ... fb fc fd fe ff"
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")
Expand Down

0 comments on commit b0602d2

Please sign in to comment.