| 1 | module hex |
| 2 | |
| 3 | const hex_digits = '0123456789abcdef' |
| 4 | const hex_digits_upper = '0123456789ABCDEF' |
| 5 | |
| 6 | // EncodeParams configures optional output formatting for encode. |
| 7 | // |
| 8 | // If `uppercase` is set, `encode` emits `A-F` instead of `a-f`. |
| 9 | // If `with_prefix` is non-empty, `encode` prepends that exact string. |
| 10 | @[params] |
| 11 | pub struct EncodeParams { |
| 12 | pub mut: |
| 13 | uppercase bool |
| 14 | with_prefix string |
| 15 | } |
| 16 | |
| 17 | // decode converts a hex string into an array of bytes. |
| 18 | // The expected input format is 2 ASCII characters for each output byte. |
| 19 | // If the provided string length is not a multiple of 2, the first digit is |
| 20 | // decoded as if an implicit `0` preceded it. An optional `0x` or `0X` prefix |
| 21 | // is accepted. |
| 22 | @[direct_array_access] |
| 23 | pub fn decode(s string) ![]u8 { |
| 24 | if s.len == 0 { |
| 25 | return []u8{} |
| 26 | } |
| 27 | |
| 28 | mut offset := 0 |
| 29 | mut hex_bytes := if s.len >= 2 { |
| 30 | if s[0] == `0` && (s[1] == `x` || s[1] == `X`) { |
| 31 | offset = 2 |
| 32 | s[2..].bytes() |
| 33 | } else { |
| 34 | s.bytes() |
| 35 | } |
| 36 | } else { |
| 37 | s.bytes() |
| 38 | } |
| 39 | |
| 40 | if hex_bytes.len == 0 { |
| 41 | return []u8{} |
| 42 | } |
| 43 | |
| 44 | mut bytes := []u8{cap: (hex_bytes.len + 1) >> 1} |
| 45 | mut start := 0 |
| 46 | if hex_bytes.len & 1 == 1 { |
| 47 | bytes << char2nibble(hex_bytes[0], offset)! |
| 48 | start = 1 |
| 49 | } |
| 50 | |
| 51 | for i := start; i < hex_bytes.len; i += 2 { |
| 52 | n1 := char2nibble(hex_bytes[i], offset + i)! |
| 53 | n0 := char2nibble(hex_bytes[i + 1], offset + i + 1)! |
| 54 | bytes << (n1 << 4) | n0 |
| 55 | } |
| 56 | return bytes |
| 57 | } |
| 58 | |
| 59 | // encode converts an array of bytes into a string of ASCII hex bytes. The |
| 60 | // output will always be a string whose length will be a multiple of 2. |
| 61 | // If `EncodeParams.uppercase` is set, the output hex characters are emitted in |
| 62 | // uppercase. |
| 63 | // If `EncodeParams.with_prefix` is non-empty, the output string is prefixed |
| 64 | // with the provided string. |
| 65 | @[direct_array_access] |
| 66 | pub fn encode(bytes []u8, params EncodeParams) string { |
| 67 | if bytes.len == 0 { |
| 68 | return '' |
| 69 | } |
| 70 | mut res := []u8{} |
| 71 | if params.with_prefix != '' { |
| 72 | res << params.with_prefix.bytes() |
| 73 | } |
| 74 | for _, b in bytes { |
| 75 | res << nibble2char(b >> 4, params) |
| 76 | res << nibble2char(b & 0xf, params) |
| 77 | } |
| 78 | return res.bytestr() |
| 79 | } |
| 80 | |
| 81 | // nibble2char converts a 4-bit hex value to its ASCII character |
| 82 | @[inline] |
| 83 | fn nibble2char(nibble u8, params EncodeParams) u8 { |
| 84 | if params.uppercase { |
| 85 | return hex_digits_upper[nibble] |
| 86 | } |
| 87 | return hex_digits[nibble] |
| 88 | } |
| 89 | |
| 90 | // char2nibble converts an ASCII hex character to its hex value |
| 91 | @[inline] |
| 92 | fn char2nibble(b u8, index int) !u8 { |
| 93 | match b { |
| 94 | `0`...`9` { return b - u8(`0`) } |
| 95 | `A`...`F` { return b - u8(`A`) + 10 } |
| 96 | `a`...`f` { return b - u8(`a`) + 10 } |
| 97 | else { return error('invalid hex char ${b.ascii_str()} at index ${index}') } |
| 98 | } |
| 99 | } |
| 100 | |