-
Notifications
You must be signed in to change notification settings - Fork 656
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Adding ByteBuffer.hexDump in xxd and hexdump -C formats #2475
Merged
Merged
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
3de39bb
Initial implementation of hexdump for ByteBuffer
natikgadzhi fb8d173
Put that iff back there
natikgadzhi 066d7e0
Reword hexDumpShort(): naming, tests, offset and full length support
natikgadzhi e89714b
Use writerIndex to figure out the convinience end of useful content i…
natikgadzhi 5e5dede
hexDumpLong(offset:) — supports offset, and reads full buffer slice, …
natikgadzhi 64e3089
hexDumpLong() extra line failing test
natikgadzhi 4134644
Using ByteBuffer(string:) instead of the allocator
natikgadzhi b640602
Move hexDumps code into ByteBuffer-hexdump.swift
natikgadzhi 006bf1d
Optimized String(byte: poadding:)
natikgadzhi 3e0573f
Avoid using storage.dumpBytes in hexDumpShort()
natikgadzhi a31717e
Implemented a cleaner long hexdump
natikgadzhi f188f67
Implemented the public hexdump API
natikgadzhi 6716354
Revert ByteBuffer-core.swift to main
natikgadzhi 5edc450
Addressed review feedback!
natikgadzhi 2f0d956
hexdump long format with clipped length
natikgadzhi c710f94
Merge branch 'main' into feature/byte-buffer-hexdump
natikgadzhi fbd8c26
reserve capacity in hexDumpLines
natikgadzhi 849e577
Addressing review feedback
natikgadzhi bea0e15
Merge branch 'main' into feature/byte-buffer-hexdump
weissi 74857cc
String(_ value, radix, padding) and the final test case
natikgadzhi 716a455
Make the hexDump(format: .detailed) format cleaner
natikgadzhi b62ca3e
Spell out if let for Swift 5.8
natikgadzhi 5b2d97c
Fixed a formatting bug in multiline maxBytes detailed format
natikgadzhi 75a82b4
Apply suggestions from code review
natikgadzhi d057109
Cleaner way to dump a line in .detailed format
natikgadzhi 56abf58
Merge branch 'main' into feature/byte-buffer-hexdump
natikgadzhi 96c077b
Cleaner generic argument type for String(_, radix:, padding:)
natikgadzhi 47bebae
Revert _storage.dumpBytes test to match previous format
natikgadzhi dc65676
Simplify printable bytes closure to a single expression so Swift 5.6 …
natikgadzhi c26275b
Merge branch 'main' into feature/byte-buffer-hexdump
Lukasa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftNIO open source project | ||
// | ||
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
extension ByteBuffer { | ||
|
||
/// Describes a ByteBuffer hexDump format. | ||
/// Can be either xxd output compatible, or hexdump compatible. | ||
public struct HexDumpFormat: Hashable, Sendable { | ||
|
||
enum Value: Hashable { | ||
case plain(maxBytes: Int? = nil) | ||
case detailed(maxBytes: Int? = nil) | ||
} | ||
|
||
let value: Value | ||
init(_ value: Value) { self.value = value } | ||
|
||
/// A plain hex dump format compatible with `xxd` CLI utility. | ||
public static let plain = Self(.plain(maxBytes: nil)) | ||
|
||
/// A hex dump format compatible with `hexdump` command line utility. | ||
public static let detailed = Self(.detailed(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 { | ||
Self(.plain(maxBytes: maxBytes)) | ||
} | ||
|
||
/// A hex dump format compatible with `hexdump` command line tool. | ||
/// This format will dump first `maxBytes / 2` bytes, and the last `maxBytes / 2` bytes, with a placeholder in between. | ||
public static func detailed(maxBytes: Int) -> Self { | ||
Self(.detailed(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 { | ||
var hexString = "" | ||
hexString.reserveCapacity(self.readableBytes * 3) | ||
|
||
for byte in self.readableBytesView { | ||
hexString += String(byte, radix: 16, padding: 2) | ||
hexString += " " | ||
} | ||
|
||
return String(hexString.dropLast()) | ||
} | ||
|
||
/// 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 { | ||
// 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.hexDumpPlain() | ||
let endHex = back.hexDumpPlain() | ||
return startHex + " ... " + endHex | ||
} | ||
|
||
/// Returns a `String` containing a detailed hex dump of this buffer. | ||
/// Intended to be used internally in ``hexDump(format:)`` | ||
/// - parameters: | ||
/// - lineOffset: an offset from the beginning of the outer buffer that is being dumped. It's used to print the line offset in hexdump -C format. | ||
/// - paddingBefore: the amount of space to pad before the first byte dumped on this line, used in center and right columns. | ||
/// - paddingAfter: the amount of sapce to pad after the last byte on this line, used in center and right columns. | ||
private func _hexDumpLine(lineOffset: Int, paddingBefore: Int = 0, paddingAfter: Int = 0) -> String { | ||
// Each line takes 78 visible characters + \n | ||
var result = "" | ||
result.reserveCapacity(79) | ||
|
||
// Left column of the hex dump signifies the offset from the beginning of the dumped buffer | ||
// and is separated from the next column with two spaces. | ||
result += String(lineOffset, radix: 16, padding: 8) | ||
result += " " | ||
|
||
// Center column consists of: | ||
// - xxd-compatible dump of the first 8 bytes | ||
// - space | ||
// - xxd-compatible dump of the rest 8 bytes | ||
// If there are not enough bytes to dump, the column is padded with space. | ||
|
||
// If there's any padding on the left, apply that first. | ||
result += String(repeating: " ", count: paddingBefore * 3) | ||
|
||
// Add the left side of the central column | ||
let bytesInLeftColumn = max(8 - paddingBefore, 0) | ||
for byte in self.readableBytesView.prefix(bytesInLeftColumn) { | ||
result += String(byte, radix: 16, padding: 2) | ||
result += " " | ||
} | ||
|
||
// Add an extra space for the centre column. | ||
result += " " | ||
|
||
// Add the right side of the central column. | ||
for byte in self.readableBytesView.dropFirst(bytesInLeftColumn) { | ||
result += String(byte, radix: 16, padding: 2) | ||
result += " " | ||
} | ||
|
||
// Pad the resulting center column line to 60 characters. | ||
result += String(repeating: " ", count: 60 - result.count) | ||
|
||
// Right column renders the 16 bytes line as ASCII characters, or "." if the character is not printable. | ||
let printableBytes = self.readableBytesView.map { | ||
let printableRange = UInt8(ascii: " ") ..< UInt8(ascii: "~") | ||
return printableRange.contains($0) ? $0 : UInt8(ascii: ".") | ||
} | ||
|
||
result += "|" | ||
result += String(repeating: " ", count: paddingBefore) | ||
result += String(decoding: printableBytes, as: UTF8.self) | ||
result += String(repeating: " ", count: paddingAfter) | ||
result += "|\n" | ||
return result | ||
} | ||
|
||
/// Returns a `String` of hexadecimal digits of bytes in the Buffer, | ||
/// with formatting compatible with output of `hexdump -C`. | ||
private func hexdumpDetailed() -> String { | ||
if self.readableBytes == 0 { | ||
return "" | ||
} | ||
|
||
var result = "" | ||
Lukasa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
result.reserveCapacity(self.readableBytes / 16 * 79 + 8) | ||
|
||
var buffer = self | ||
|
||
var lineOffset = 0 | ||
while buffer.readableBytes > 0 { | ||
// Safe to force-unwrap because we're in a loop that guarantees there's at least one byte to read. | ||
let slice = buffer.readSlice(length: min(16, buffer.readableBytes))! | ||
result += slice._hexDumpLine(lineOffset: lineOffset) | ||
lineOffset += slice.readableBytes | ||
} | ||
|
||
result += String(self.readableBytes, radix: 16, padding: 8) | ||
return result | ||
} | ||
|
||
/// Returns a `String` of hexadecimal digits of bytes in this ByteBuffer | ||
/// with formatting sort of compatible with `hexdump -C`, but clipped on length. | ||
/// Dumps limit/2 first and limit/2 last bytes, with a separator line in between. | ||
/// | ||
/// - parameters: | ||
/// - maxBytes: Max bytes to dump. | ||
private func hexDumpDetailed(maxBytes: Int) -> String { | ||
if self.readableBytes <= maxBytes { | ||
return self.hexdumpDetailed() | ||
} | ||
|
||
let separator = "........ .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..................\n" | ||
|
||
// reserve capacity for the maxBytes dumped, plus the separator line, and buffer length line. | ||
var result = "" | ||
result.reserveCapacity(maxBytes/16 * 79 + 79 + 8) | ||
|
||
var buffer = self | ||
|
||
// Dump the front part of the buffer first, up to maxBytes/2 bytes. | ||
// Safe to force-unwrap because we know the buffer has more readable bytes than maxBytes. | ||
var front = buffer.readSlice(length: maxBytes / 2)! | ||
var bufferOffset = 0 | ||
while front.readableBytes > 0 { | ||
// Safe to force-unwrap because buffer is guaranteed to have at least one byte in it. | ||
let slice = front.readSlice(length: min(16, front.readableBytes))! | ||
|
||
// This will only be non-zero on the last line of this loop | ||
let paddingAfter = 16 - slice.readableBytes | ||
result += slice._hexDumpLine(lineOffset: bufferOffset, paddingAfter: paddingAfter) | ||
bufferOffset += slice.readableBytes | ||
} | ||
|
||
result += separator | ||
|
||
// Dump the back maxBytes/2 bytes. | ||
bufferOffset = buffer.writerIndex - maxBytes / 2 | ||
buffer.moveReaderIndex(to: bufferOffset) | ||
var back = buffer.readSlice(length: buffer.readableBytes)! | ||
|
||
// On the first line of the back part, we might want less than 16 bytes, with padding on the left. | ||
// But if this is also the last line, than take whatever is left. | ||
let lineLength = min(16 - bufferOffset % 16, back.readableBytes) | ||
|
||
// Line offset is the offset of the first byte of this line in a full buffer hex dump. | ||
// It may not match `bufferOffset` in the first line of the `back` part. | ||
let lineOffset = bufferOffset - bufferOffset % 16 | ||
|
||
// Safe to force-unwrap because `back` is guaranteed to have at least one byte. | ||
let slice = back.readSlice(length: lineLength)! | ||
|
||
// paddingBefore is going to be applied both in the center column and the right column of the line. | ||
result += slice._hexDumpLine(lineOffset: lineOffset, paddingBefore: 16 - lineLength) | ||
bufferOffset += lineLength | ||
|
||
// Now dump the rest of the back part of the buffer. | ||
while back.readableBytes > 0 { | ||
let slice = back.readSlice(length: min(16, back.readableBytes))! | ||
result += slice._hexDumpLine(lineOffset: bufferOffset) | ||
bufferOffset += slice.readableBytes | ||
} | ||
|
||
// Last line of the dump, just the index of the last byte in the buffer. | ||
result += String(self.readableBytes, radix: 16, padding: 8) | ||
return result | ||
} | ||
|
||
/// Returns a hex dump of this `ByteBuffer` in a preferred `HexDumpFormat`. | ||
/// | ||
/// `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. | ||
/// - `.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. | ||
/// | ||
/// - parameters: | ||
/// - format: ``HexDumpFormat`` to use for the dump. | ||
public func hexDump(format: HexDumpFormat) -> String { | ||
switch(format.value) { | ||
case .plain(let maxBytes): | ||
if let maxBytes = maxBytes { | ||
return self.hexDumpPlain(maxBytes: maxBytes) | ||
} else { | ||
return self.hexDumpPlain() | ||
} | ||
|
||
case .detailed(let maxBytes): | ||
if let maxBytes = maxBytes { | ||
return self.hexDumpDetailed(maxBytes: maxBytes) | ||
} else { | ||
return self.hexdumpDetailed() | ||
} | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1843,19 +1843,108 @@ class ByteBufferTest: XCTestCase { | |
} | ||
let actual = self.buf._storage.dumpBytes(slice: self.buf._slice, offset: 0, length: self.buf.readableBytes) | ||
let expected = """ | ||
[ \ | ||
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f \ | ||
20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f \ | ||
40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f \ | ||
60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f \ | ||
80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f 90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f \ | ||
a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf \ | ||
c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df \ | ||
e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff ] | ||
e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff | ||
""" | ||
XCTAssertEqual(expected, actual) | ||
} | ||
|
||
func testHexDumpPlain() { | ||
let buf = ByteBuffer(string: "Hello") | ||
XCTAssertEqual("48 65 6c 6c 6f", buf.hexDump(format: .plain)) | ||
} | ||
|
||
func testHexDumpPlainEmptyBuffer() { | ||
let buf = ByteBuffer(string: "") | ||
XCTAssertEqual("", buf.hexDump(format: .plain)) | ||
} | ||
|
||
func testHexDumpPlainWithReaderIndexOffset() { | ||
var buf = ByteBuffer(string: "Hello") | ||
let firstTwo = buf.readBytes(length: 2)! | ||
XCTAssertEqual([72, 101], firstTwo) | ||
XCTAssertEqual("6c 6c 6f", buf.hexDump(format: .plain)) | ||
} | ||
|
||
func testHexDumpPlainWithMaxBytes() { | ||
self.buf.clear() | ||
for f in UInt8.min...UInt8.max { | ||
self.buf.writeInteger(f) | ||
} | ||
let actual = self.buf.hexDump(format: .plain(maxBytes: 10)) | ||
let expected = "00 01 02 03 04 ... fb fc fd fe ff" | ||
XCTAssertEqual(expected, actual) | ||
} | ||
|
||
func testHexDumpDetailed() { | ||
let buf = ByteBuffer(string: "Goodbye, world! It was nice knowing you.\n") | ||
let expected = """ | ||
00000000 47 6f 6f 64 62 79 65 2c 20 77 6f 72 6c 64 21 20 |Goodbye, world! | | ||
glbrntt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
00000010 49 74 20 77 61 73 20 6e 69 63 65 20 6b 6e 6f 77 |It was nice know| | ||
00000020 69 6e 67 20 79 6f 75 2e 0a |ing you..| | ||
00000029 | ||
""" | ||
let actual = buf.hexDump(format: .detailed) | ||
XCTAssertEqual(expected, actual) | ||
} | ||
|
||
|
||
func testHexDumpDetailedWithMaxBytes() { | ||
let buf = ByteBuffer(string: "Goodbye, world! It was nice knowing you.\n") | ||
let expected = """ | ||
00000000 47 6f 6f 64 62 79 65 2c |Goodbye, | | ||
........ .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .................. | ||
00000020 6e 67 20 79 6f 75 2e 0a | ng you..| | ||
00000029 | ||
""" | ||
let actual = buf.hexDump(format: .detailed(maxBytes: 16)) | ||
XCTAssertEqual(expected, actual) | ||
} | ||
|
||
func testHexDumpDetailedWithMultilineFrontAndBack() { | ||
let buf = ByteBuffer(string: """ | ||
Goodbye, world! It was nice knowing you. | ||
I will miss this pull request with all of it's 94+ comments. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can keep it going if you like? 😅 (Also, thank you again, this is a really awesome addition!) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. such good changes |
||
""") | ||
|
||
let expected = """ | ||
00000000 47 6f 6f 64 62 79 65 2c 20 77 6f 72 6c 64 21 20 |Goodbye, world! | | ||
00000010 49 74 |It | | ||
........ .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .................. | ||
00000050 69 74 27 73 20 39 34 2b 20 63 6f 6d 6d | it's 94+ comm| | ||
00000060 65 6e 74 73 2e |ents.| | ||
00000065 | ||
""" | ||
let actual = buf.hexDump(format: .detailed(maxBytes: 36)) | ||
XCTAssertEqual(expected, actual) | ||
} | ||
|
||
func testHexDumpDetailedWithOffset() { | ||
var buf = ByteBuffer(string: "Goodbye, world! It was nice knowing you.\n") | ||
let _ = buf.readBytes(length: 5) | ||
let expected = """ | ||
00000000 79 65 2c 20 77 6f 72 6c 64 21 20 49 74 20 77 61 |ye, world! It wa| | ||
00000010 73 20 6e 69 63 65 20 6b 6e 6f 77 69 6e 67 20 79 |s nice knowing y| | ||
00000020 6f 75 2e 0a |ou..| | ||
00000024 | ||
""" | ||
let actual = buf.hexDump(format: .detailed) | ||
XCTAssertEqual(expected, actual) | ||
} | ||
|
||
func testHexDumpLongEmptyBuffer() { | ||
let buf = ByteBuffer() | ||
let expected = "" | ||
let actual = buf.hexDump(format: .detailed) | ||
XCTAssertEqual(expected, actual) | ||
} | ||
|
||
func testReadableBytesView() throws { | ||
self.buf.clear() | ||
self.buf.writeString("hello world 012345678") | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we give this a better generic type parameter name? Maybe
Value
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. Done in 96c077b.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nicely done