Repository: apple/swift-nio
File: Sources/NIOCore/ByteBuffer-hex.swift (Swift)

Code before:
    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()
            }
        }
    }
}

extension String {
    /// Plain decode a string representing a hexadecimal sequence them into an UInt8 sequence
    /// - Complexity: O(n)
    @inlinable
    var hexPlainDecodedBytes: UnfoldSequence<UInt8, String> {
        let stringWithoutWhiteSpaces = self.filter { !$0.isWhitespace }
        return sequence(
            state: stringWithoutWhiteSpaces,
            next: { remainder in
                guard remainder.count >= 2 else {
                    precondition(remainder.count == 0, "Uneven number of hex encoded bytes within string")
                    return nil
                }
                let nextTwo = remainder.prefix(2)
                remainder.removeFirst(2)
                return UInt8(nextTwo, radix: 16)
            }
        )
    }
}


Review comment (suggestion):
I think this might be a bit too clever, and I wonder if we should try a simpler pattern. Instead of doing a `filter` followed by a `sequence`, all working on the `String`, can I propose we do a for loop? Something like this:

```swift
extension UInt8 {
    fileprivate var isASCIIWhitespace: Bool {
        switch self {
        case UInt8(ascii: "\n"), UInt8(ascii: "\t"), UInt8(ascii: "\n"), UInt8(ascii: "\r"):
            return true
        default: return false
        }
    }

    var asciiHexNibble: UInt8? {
        switch self {
        case UInt8(ascii: "0")...UInt8(ascii: "9"):
            return self - UInt8(ascii: "0")
        case UInt8(ascii: "a")...UInt8(ascii: "f"):
            return self - UInt8(ascii: "a")
        case UInt8(ascii: "A")...UInt8(ascii: "F"):
            return self - UInt8(ascii: "A")
        default:
            return nil
        }
    }
}

extension Substring.UTF8View {
    mutating func popNextHexByte() -> UInt8? {
        while let nextByte = self.first, nextByte.isASCIIWhitespace {
            self = self.dropFirst()
        }
        
        guard let firstHex = self.popFirst()?.asciiHexNibble, 
            let secondHex = self.popFirst()?.asciiHexNibble else {
            return nil
        }
        
        return (firstHex << 4) | secondHex
    }
}

extension ByteBuffer {
    mutating func writeHexEncodedBytes(_ hexString: String) -> Int {
        var slice = hexString.utf8[...]
        var written = 0
        while let nextByte = slice.popNextHexByte() {
            written += self.writeInteger(nextByte)
        }
        return written
    }
}
```

Code after:

        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
        }