/src/swift-nio/Sources/NIOCore/ByteBuffer-hex.swift
Line | Count | Source |
1 | | //===----------------------------------------------------------------------===// |
2 | | // |
3 | | // This source file is part of the SwiftNIO open source project |
4 | | // |
5 | | // Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors |
6 | | // Licensed under Apache License v2.0 |
7 | | // |
8 | | // See LICENSE.txt for license information |
9 | | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors |
10 | | // |
11 | | // SPDX-License-Identifier: Apache-2.0 |
12 | | // |
13 | | //===----------------------------------------------------------------------===// |
14 | | |
15 | | extension ByteBuffer { |
16 | | |
17 | | /// Create a fresh `ByteBuffer` containing the `bytes` decoded from the string representation of `plainHexEncodedBytes`. |
18 | | /// |
19 | | /// This will allocate a new `ByteBuffer` with enough space to fit the hex decoded `bytes` and potentially some extra space |
20 | | /// using the default allocator. |
21 | | /// |
22 | | /// - info: If you have access to a `Channel`, `ChannelHandlerContext`, or `ByteBufferAllocator` we |
23 | | /// recommend using `channel.allocator.buffer(integer:)`. Or if you want to write multiple items into the |
24 | | /// buffer use `channel.allocator.buffer(capacity: ...)` to allocate a `ByteBuffer` of the right |
25 | | /// size followed by a `writeHexEncodedBytes` instead of using this method. This allows SwiftNIO to do |
26 | | /// accounting and optimisations of resources acquired for operations on a given `Channel` in the future. |
27 | 0 | public init(plainHexEncodedBytes string: String) throws { |
28 | 0 | self = try ByteBufferAllocator().buffer(plainHexEncodedBytes: string) |
29 | 0 | } |
30 | | |
31 | | /// Describes a ByteBuffer hexDump format. |
32 | | /// Can be either xxd output compatible, or hexdump compatible. |
33 | | public struct HexDumpFormat: Hashable, Sendable { |
34 | | |
35 | | @usableFromInline |
36 | | enum Value: Hashable, Sendable { |
37 | | case plain(maxBytes: Int? = nil) |
38 | | case detailed(maxBytes: Int? = nil) |
39 | | case compact(maxBytes: Int? = nil) |
40 | | } |
41 | | |
42 | | @usableFromInline |
43 | | let value: Value |
44 | | |
45 | | @inlinable |
46 | 0 | init(_ value: Value) { self.value = value } |
47 | | |
48 | | /// A plain hex dump format compatible with `xxd` CLI utility. |
49 | | @inlinable |
50 | 0 | public static var plain: HexDumpFormat { Self(.plain(maxBytes: nil)) } |
51 | | |
52 | | /// A hex dump format compatible with `hexdump` command line utility. |
53 | | @inlinable |
54 | 0 | public static var detailed: HexDumpFormat { Self(.detailed(maxBytes: nil)) } |
55 | | |
56 | | /// A hex dump analog to `plain` format but without whitespaces. |
57 | 0 | public static var compact: HexDumpFormat { Self(.compact(maxBytes: nil)) } |
58 | | |
59 | | /// A detailed hex dump format compatible with `xxd`, clipped to `maxBytes` bytes dumped. |
60 | | /// This format will dump first `maxBytes / 2` bytes, and the last `maxBytes / 2` bytes, replacing the rest with " ... ". |
61 | 0 | public static func plain(maxBytes: Int) -> Self { |
62 | 0 | Self(.plain(maxBytes: maxBytes)) |
63 | 0 | } |
64 | | |
65 | | /// A hex dump format compatible with `hexdump` command line tool. |
66 | | /// This format will dump first `maxBytes / 2` bytes, and the last `maxBytes / 2` bytes, with a placeholder in between. |
67 | 0 | public static func detailed(maxBytes: Int) -> Self { |
68 | 0 | Self(.detailed(maxBytes: maxBytes)) |
69 | 0 | } |
70 | | |
71 | | /// A hex dump analog to `plain`format but without whitespaces. |
72 | | /// This format will dump first `maxBytes / 2` bytes, and the last `maxBytes / 2` bytes, with a placeholder in between. |
73 | 0 | public static func compact(maxBytes: Int) -> Self { |
74 | 0 | Self(.compact(maxBytes: maxBytes)) |
75 | 0 | } |
76 | | } |
77 | | |
78 | | /// Shared logic for `hexDumpPlain` and `hexDumpCompact`. |
79 | | /// Returns a `String` of hexadecimals digits of the readable bytes in the buffer. |
80 | | /// - Parameter |
81 | | /// - separateWithWhitespace: Controls whether the hex deump will be separated by whitespaces. |
82 | 0 | private func _hexDump(separateWithWhitespace: Bool) -> String { |
83 | 0 | var hexString = "" |
84 | 0 | var capacity: Int |
85 | 0 |
|
86 | 0 | if separateWithWhitespace { |
87 | 0 | capacity = self.readableBytes * 3 |
88 | 0 | } else { |
89 | 0 | capacity = self.readableBytes * 2 |
90 | 0 | } |
91 | 0 |
|
92 | 0 | hexString.reserveCapacity(capacity) |
93 | 0 |
|
94 | 0 | for byte in self.readableBytesView { |
95 | 0 | hexString += String(byte, radix: 16, padding: 2) |
96 | 0 | if separateWithWhitespace { |
97 | 0 | hexString += " " |
98 | 0 | } |
99 | 0 | } |
100 | 0 |
|
101 | 0 | if separateWithWhitespace { |
102 | 0 | return String(hexString.dropLast()) |
103 | 0 | } |
104 | 0 |
|
105 | 0 | return hexString |
106 | 0 | } |
107 | | |
108 | | /// Shared logic for `hexDumpPlain(maxBytes: Int)` and `hexDumpCompact(maxBytes: Int)`. |
109 | | /// |
110 | | /// - Parameters: |
111 | | /// - maxBytes: The maximum amount of bytes presented in the dump. |
112 | | /// - separateWithWhitespace: Controls whether the dump will be separated by whitespaces. |
113 | 0 | private func _hexDump(maxBytes: Int, separateWithWhitespace: Bool) -> String { |
114 | 0 | // If the buffer length fits in the max bytes limit in the hex dump, just dump the whole thing. |
115 | 0 | if self.readableBytes <= maxBytes { |
116 | 0 | return self._hexDump(separateWithWhitespace: separateWithWhitespace) |
117 | 0 | } |
118 | 0 |
|
119 | 0 | var buffer = self |
120 | 0 |
|
121 | 0 | // Safe to force-unwrap because we just checked readableBytes is > maxBytes above. |
122 | 0 | let front = buffer.readSlice(length: maxBytes / 2)! |
123 | 0 | buffer.moveReaderIndex(to: buffer.writerIndex - maxBytes / 2) |
124 | 0 | let back = buffer.readSlice(length: buffer.readableBytes)! |
125 | 0 |
|
126 | 0 | let startHex = front._hexDump(separateWithWhitespace: separateWithWhitespace) |
127 | 0 | let endHex = back._hexDump(separateWithWhitespace: separateWithWhitespace) |
128 | 0 |
|
129 | 0 | var dots: String |
130 | 0 | if separateWithWhitespace { |
131 | 0 | dots = " ... " |
132 | 0 | } else { |
133 | 0 | dots = "..." |
134 | 0 | } |
135 | 0 |
|
136 | 0 | return startHex + dots + endHex |
137 | 0 | } |
138 | | |
139 | | /// Return a `String` of space separated hexadecimal digits of the readable bytes in the buffer, |
140 | | /// in a format that's compatible with `xxd -r -p`. |
141 | | /// `hexDumpPlain()` always dumps all readable bytes, i.e. from `readerIndex` to `writerIndex`, |
142 | | /// so you should set those indices to desired location to get the offset and length that you need to dump. |
143 | 0 | private func hexDumpPlain() -> String { |
144 | 0 | self._hexDump(separateWithWhitespace: true) |
145 | 0 | } |
146 | | |
147 | | /// Return a `String` of space delimited hexadecimal digits of the readable bytes in the buffer, |
148 | | /// in a format that's compatible with `xxd -r -p`, but clips the output to the max length of `maxBytes` bytes. |
149 | | /// If the dump contains more than the `maxBytes` bytes, this function will return the first `maxBytes/2` |
150 | | /// and the last `maxBytes/2` of that, replacing the rest with `...`, i.e. `01 02 03 ... 09 11 12`. |
151 | | /// |
152 | | /// - Parameters: |
153 | | /// - maxBytes: The maximum amount of bytes presented in the dump. |
154 | 0 | private func hexDumpPlain(maxBytes: Int) -> String { |
155 | 0 | self._hexDump(maxBytes: maxBytes, separateWithWhitespace: true) |
156 | 0 | } |
157 | | |
158 | | /// Return a `String` of hexadecimal digits of the readable bytes in the buffer, |
159 | | /// analog to `.plain` format but without whitespaces. This format guarantees not to emit whitespaces. |
160 | | /// `hexDumpCompact()` always dumps all readable bytes, i.e. from `readerIndex` to `writerIndex`, |
161 | | /// so you should set those indices to desired location to get the offset and length that you need to dump. |
162 | 0 | private func hexDumpCompact() -> String { |
163 | 0 | self._hexDump(separateWithWhitespace: false) |
164 | 0 | } |
165 | | |
166 | | /// Return a `String` of hexadecimal digits of the readable bytes in the buffer, |
167 | | /// analog to `.plain` format but without whitespaces and clips the output to the max length of `maxBytes` bytes. |
168 | | /// This format guarantees not to emmit whitespaces. |
169 | | /// If the dump contains more than the `maxBytes` bytes, this function will return the first `maxBytes/2` |
170 | | /// and the last `maxBytes/2` of that, replacing the rest with `...`, i.e. `010203...091112`. |
171 | | /// |
172 | | /// - Parameters: |
173 | | /// - maxBytes: The maximum amount of bytes presented in the dump. |
174 | 0 | private func hexDumpCompact(maxBytes: Int) -> String { |
175 | 0 | self._hexDump(maxBytes: maxBytes, separateWithWhitespace: false) |
176 | 0 | } |
177 | | |
178 | | /// Returns a `String` containing a detailed hex dump of this buffer. |
179 | | /// Intended to be used internally in ``hexDump(format:)`` |
180 | | /// - Parameters: |
181 | | /// - 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. |
182 | | /// - paddingBefore: the amount of space to pad before the first byte dumped on this line, used in center and right columns. |
183 | | /// - paddingAfter: the amount of sapce to pad after the last byte on this line, used in center and right columns. |
184 | 0 | private func _hexDumpLine(lineOffset: Int, paddingBefore: Int = 0, paddingAfter: Int = 0) -> String { |
185 | 0 | // Each line takes 78 visible characters + \n |
186 | 0 | var result = "" |
187 | 0 | result.reserveCapacity(79) |
188 | 0 |
|
189 | 0 | // Left column of the hex dump signifies the offset from the beginning of the dumped buffer |
190 | 0 | // and is separated from the next column with two spaces. |
191 | 0 | result += String(lineOffset, radix: 16, padding: 8) |
192 | 0 | result += " " |
193 | 0 |
|
194 | 0 | // Center column consists of: |
195 | 0 | // - xxd-compatible dump of the first 8 bytes |
196 | 0 | // - space |
197 | 0 | // - xxd-compatible dump of the rest 8 bytes |
198 | 0 | // If there are not enough bytes to dump, the column is padded with space. |
199 | 0 |
|
200 | 0 | // If there's any padding on the left, apply that first. |
201 | 0 | result += String(repeating: " ", count: paddingBefore * 3) |
202 | 0 |
|
203 | 0 | // Add the left side of the central column |
204 | 0 | let bytesInLeftColumn = max(8 - paddingBefore, 0) |
205 | 0 | for byte in self.readableBytesView.prefix(bytesInLeftColumn) { |
206 | 0 | result += String(byte, radix: 16, padding: 2) |
207 | 0 | result += " " |
208 | 0 | } |
209 | 0 |
|
210 | 0 | // Add an extra space for the centre column. |
211 | 0 | result += " " |
212 | 0 |
|
213 | 0 | // Add the right side of the central column. |
214 | 0 | for byte in self.readableBytesView.dropFirst(bytesInLeftColumn) { |
215 | 0 | result += String(byte, radix: 16, padding: 2) |
216 | 0 | result += " " |
217 | 0 | } |
218 | 0 |
|
219 | 0 | // Pad the resulting center column line to 60 characters. |
220 | 0 | result += String(repeating: " ", count: 60 - result.count) |
221 | 0 |
|
222 | 0 | // Right column renders the 16 bytes line as ASCII characters, or "." if the character is not printable. |
223 | 0 | let printableRange = UInt8(ascii: " ")..<UInt8(ascii: "~") |
224 | 0 | let printableBytes = self.readableBytesView.map { |
225 | 0 | printableRange.contains($0) ? $0 : UInt8(ascii: ".") |
226 | 0 | } |
227 | 0 |
|
228 | 0 | result += "|" |
229 | 0 | result += String(repeating: " ", count: paddingBefore) |
230 | 0 | result += String(decoding: printableBytes, as: UTF8.self) |
231 | 0 | result += String(repeating: " ", count: paddingAfter) |
232 | 0 | result += "|\n" |
233 | 0 | return result |
234 | 0 | } |
235 | | |
236 | | /// Returns a `String` of hexadecimal digits of bytes in the Buffer, |
237 | | /// with formatting compatible with output of `hexdump -C`. |
238 | 0 | private func hexdumpDetailed() -> String { |
239 | 0 | if self.readableBytes == 0 { |
240 | 0 | return "" |
241 | 0 | } |
242 | 0 |
|
243 | 0 | var result = "" |
244 | 0 | result.reserveCapacity(self.readableBytes / 16 * 79 + 8) |
245 | 0 |
|
246 | 0 | var buffer = self |
247 | 0 |
|
248 | 0 | var lineOffset = 0 |
249 | 0 | while buffer.readableBytes > 0 { |
250 | 0 | // Safe to force-unwrap because we're in a loop that guarantees there's at least one byte to read. |
251 | 0 | let slice = buffer.readSlice(length: min(16, buffer.readableBytes))! |
252 | 0 | result += slice._hexDumpLine(lineOffset: lineOffset) |
253 | 0 | lineOffset += slice.readableBytes |
254 | 0 | } |
255 | 0 |
|
256 | 0 | result += String(self.readableBytes, radix: 16, padding: 8) |
257 | 0 | return result |
258 | 0 | } |
259 | | |
260 | | /// Returns a `String` of hexadecimal digits of bytes in this ByteBuffer |
261 | | /// with formatting sort of compatible with `hexdump -C`, but clipped on length. |
262 | | /// Dumps limit/2 first and limit/2 last bytes, with a separator line in between. |
263 | | /// |
264 | | /// - Parameters: |
265 | | /// - maxBytes: Max bytes to dump. |
266 | 0 | private func hexDumpDetailed(maxBytes: Int) -> String { |
267 | 0 | if self.readableBytes <= maxBytes { |
268 | 0 | return self.hexdumpDetailed() |
269 | 0 | } |
270 | 0 |
|
271 | 0 | let separator = "........ .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..................\n" |
272 | 0 |
|
273 | 0 | // reserve capacity for the maxBytes dumped, plus the separator line, and buffer length line. |
274 | 0 | var result = "" |
275 | 0 | result.reserveCapacity(maxBytes / 16 * 79 + 79 + 8) |
276 | 0 |
|
277 | 0 | var buffer = self |
278 | 0 |
|
279 | 0 | // Dump the front part of the buffer first, up to maxBytes/2 bytes. |
280 | 0 | // Safe to force-unwrap because we know the buffer has more readable bytes than maxBytes. |
281 | 0 | var front = buffer.readSlice(length: maxBytes / 2)! |
282 | 0 | var bufferOffset = 0 |
283 | 0 | while front.readableBytes > 0 { |
284 | 0 | // Safe to force-unwrap because buffer is guaranteed to have at least one byte in it. |
285 | 0 | let slice = front.readSlice(length: min(16, front.readableBytes))! |
286 | 0 |
|
287 | 0 | // This will only be non-zero on the last line of this loop |
288 | 0 | let paddingAfter = 16 - slice.readableBytes |
289 | 0 | result += slice._hexDumpLine(lineOffset: bufferOffset, paddingAfter: paddingAfter) |
290 | 0 | bufferOffset += slice.readableBytes |
291 | 0 | } |
292 | 0 |
|
293 | 0 | result += separator |
294 | 0 |
|
295 | 0 | // Dump the back maxBytes/2 bytes. |
296 | 0 | bufferOffset = buffer.writerIndex - maxBytes / 2 |
297 | 0 | buffer.moveReaderIndex(to: bufferOffset) |
298 | 0 | var back = buffer.readSlice(length: buffer.readableBytes)! |
299 | 0 |
|
300 | 0 | // On the first line of the back part, we might want less than 16 bytes, with padding on the left. |
301 | 0 | // But if this is also the last line, than take whatever is left. |
302 | 0 | let lineLength = min(16 - bufferOffset % 16, back.readableBytes) |
303 | 0 |
|
304 | 0 | // Line offset is the offset of the first byte of this line in a full buffer hex dump. |
305 | 0 | // It may not match `bufferOffset` in the first line of the `back` part. |
306 | 0 | let lineOffset = bufferOffset - bufferOffset % 16 |
307 | 0 |
|
308 | 0 | // Safe to force-unwrap because `back` is guaranteed to have at least one byte. |
309 | 0 | let slice = back.readSlice(length: lineLength)! |
310 | 0 |
|
311 | 0 | // paddingBefore is going to be applied both in the center column and the right column of the line. |
312 | 0 | result += slice._hexDumpLine(lineOffset: lineOffset, paddingBefore: 16 - lineLength) |
313 | 0 | bufferOffset += lineLength |
314 | 0 |
|
315 | 0 | // Now dump the rest of the back part of the buffer. |
316 | 0 | while back.readableBytes > 0 { |
317 | 0 | let slice = back.readSlice(length: min(16, back.readableBytes))! |
318 | 0 | result += slice._hexDumpLine(lineOffset: bufferOffset) |
319 | 0 | bufferOffset += slice.readableBytes |
320 | 0 | } |
321 | 0 |
|
322 | 0 | // Last line of the dump, just the index of the last byte in the buffer. |
323 | 0 | result += String(self.readableBytes, radix: 16, padding: 8) |
324 | 0 | return result |
325 | 0 | } |
326 | | |
327 | | /// Returns a hex dump of this `ByteBuffer` in a preferred `HexDumpFormat`. |
328 | | /// |
329 | | /// `hexDump` provides several formats: |
330 | | /// - `.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`. |
331 | | /// - `.plain(maxBytes: Int)` — like `.plain`, but clipped to maximum bytes dumped. |
332 | | /// - `.compact` — plain hexd dump without whitespaces. |
333 | | /// - `.compact(maxBytes: Int)` — like `.compact`, but clipped to maximum bytes dumped. |
334 | | /// - `.detailed` — detailed hex dump format with both hex, and ASCII representation of the bytes. This format is compatible with what `hexdump -C` outputs. |
335 | | /// - `.detailed(maxBytes: Int)` — like `.detailed`, but clipped to maximum bytes dumped. |
336 | | /// |
337 | | /// - Parameters: |
338 | | /// - format: ``HexDumpFormat`` to use for the dump. |
339 | 0 | public func hexDump(format: HexDumpFormat) -> String { |
340 | 0 | switch format.value { |
341 | 0 | case .plain(let maxBytes): |
342 | 0 | if let maxBytes = maxBytes { |
343 | 0 | return self.hexDumpPlain(maxBytes: maxBytes) |
344 | 0 | } else { |
345 | 0 | return self.hexDumpPlain() |
346 | 0 | } |
347 | 0 |
|
348 | 0 | case .compact(let maxBytes): |
349 | 0 | if let maxBytes = maxBytes { |
350 | 0 | return self.hexDumpCompact(maxBytes: maxBytes) |
351 | 0 | } else { |
352 | 0 | return self.hexDumpCompact() |
353 | 0 | } |
354 | 0 |
|
355 | 0 | case .detailed(let maxBytes): |
356 | 0 | if let maxBytes = maxBytes { |
357 | 0 | return self.hexDumpDetailed(maxBytes: maxBytes) |
358 | 0 | } else { |
359 | 0 | return self.hexdumpDetailed() |
360 | 0 | } |
361 | 0 | } |
362 | 0 | } |
363 | | |
364 | | /// An error that is thrown when an invalid hex encoded string was attempted to be written to a ByteBuffer. |
365 | | public struct HexDecodingError: Error, Equatable { |
366 | | private let kind: HexDecodingErrorKind |
367 | | |
368 | | private enum HexDecodingErrorKind { |
369 | | /// The hex encoded string was not of the expected even length. |
370 | | case invalidHexLength |
371 | | /// An invalid hex character was found in the hex encoded string. |
372 | | case invalidCharacter |
373 | | } |
374 | | |
375 | | public static let invalidHexLength = HexDecodingError(kind: .invalidHexLength) |
376 | | public static let invalidCharacter = HexDecodingError(kind: .invalidCharacter) |
377 | | } |
378 | | } |
379 | | |
380 | | extension UInt8 { |
381 | 0 | fileprivate var isASCIIWhitespace: Bool { |
382 | 0 | [UInt8(ascii: "\n"), UInt8(ascii: "\t"), UInt8(ascii: "\r"), UInt8(ascii: " ")].contains( |
383 | 0 | self |
384 | 0 | ) |
385 | 0 | } |
386 | | |
387 | | fileprivate var asciiHexNibble: UInt8 { |
388 | 0 | get throws { |
389 | 0 | switch self { |
390 | 0 | case UInt8(ascii: "0")...UInt8(ascii: "9"): |
391 | 0 | return self - UInt8(ascii: "0") |
392 | 0 | case UInt8(ascii: "a")...UInt8(ascii: "f"): |
393 | 0 | return self - UInt8(ascii: "a") + 10 |
394 | 0 | case UInt8(ascii: "A")...UInt8(ascii: "F"): |
395 | 0 | return self - UInt8(ascii: "A") + 10 |
396 | 0 | default: |
397 | 0 | throw ByteBuffer.HexDecodingError.invalidCharacter |
398 | 0 | } |
399 | 0 | } |
400 | | } |
401 | | } |
402 | | |
403 | | extension Substring.UTF8View { |
404 | | @usableFromInline |
405 | 0 | mutating func popNextHexByte() throws -> UInt8? { |
406 | 0 | while let nextByte = self.first, nextByte.isASCIIWhitespace { |
407 | 0 | self = self.dropFirst() |
408 | 0 | } |
409 | 0 |
|
410 | 0 | guard let firstHex = try self.popFirst()?.asciiHexNibble else { |
411 | 0 | return nil // No next byte to pop |
412 | 0 | } |
413 | 0 |
|
414 | 0 | guard let secondHex = try self.popFirst()?.asciiHexNibble else { |
415 | 0 | throw ByteBuffer.HexDecodingError.invalidHexLength |
416 | 0 | } |
417 | 0 |
|
418 | 0 | return (firstHex << 4) | secondHex |
419 | 0 | } |
420 | | } |