Problem: you want to encode arbitrary binary data as a hashtag on Mastodon or another popular platform.

A solution: transform to a hex string and shift the 0..f up to a..p so all the bytes are lowercase Latin characters.

First, load up {tidyverse}:

library(tidyverse)

I want to transform a string like “Hello” to a hex string:

chars2hexstr <- function(t) {
  t |>
    charToRaw() |>
    as.character() |>
    paste(collapse = "")
}

Testing:

chars2hexstr("Hello")
[1] "48656c6c6f"

Consulting an ASCII table, looks good. “H” = 0x48, “e” = 0x65, “l” = 0x6c, and “o” = 0x6f.

Let’s undo it:

hexstr2chars <- function(hexstr) {
  substring(hexstr,
            seq(1, nchar(hexstr) - 1, 2),
            seq(2, nchar(hexstr), 2)) |>
  strtoi(base = 16) |>
  as.raw() |>
  rawToChar()
}

And a test:

hexstr2chars("48656c6c6f")
[1] "Hello"

Next, I want to shift 0..f up to a..p. I’m sure there’s an easier way. Here’s how I’m doing it. First, setup a mapping:

charmap <- data.frame(
  hex = c(0:9, letters[1:6]),
  let = letters[1:16])
charmap

These two functions translate back and forth between 0..f (hex) and a..p (I’m going to call it a lethex), using a strsplit and join:

hex2lethex <- function(hstr) {
  data.frame(hex = (hstr |> strsplit(""))[[1]]) |>
    left_join(charmap, by = "hex") |>
    pull(let) |>
    paste(collapse = "")
}

lethex2hex <- function(lethex) {
  data.frame(let = (lethex |> strsplit(""))[[1]]) |>
    left_join(charmap, by = "let") |>
    pull(hex) |>
    paste(collapse = "")
}

Try it with “Hello”. Here’s the hex string:

hello_hex <- chars2hexstr("Hello")
hello_hex
[1] "48656c6c6f"

Now as a lethex:

hello_lethex <- hello_hex |> hex2lethex()
hello_lethex
[1] "eigfgmgmgp"

And back again:

hello_lethex |> lethex2hex()
[1] "48656c6c6f"

Finally, a couple of wrappers, which take a string to a lethex string and back again:

str2hashtag <- function(s) {
  s |> chars2hexstr() |> hex2lethex()
}

hashtag2str <- function(h) {
  h |> lethex2hex() |> hexstr2chars()
}
str2hashtag("Hello")
[1] "eigfgmgmgp"
hashtag2str("eigfgmgmgp")
[1] "Hello"

Finally:

hashtag2str("gihehehahddkcpcphhhhhhcohjgphfhehfgcgfcogdgpgncphhgbhegdgidphgdngefbhhdehhdjfhghfigdfb")
LS0tDQp0aXRsZTogIkVuY29kZSBieXRlcyBhcyBoYXNodGFncyINCmF1dGhvcjogIkBhbmRpQHRlY2gubGdidCINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIGNvZGVfZm9sZGluZzogbm9uZQ0KLS0tDQoNCioqUHJvYmxlbToqKiB5b3Ugd2FudCB0byBlbmNvZGUgYXJiaXRyYXJ5IGJpbmFyeSBkYXRhIGFzIGEgaGFzaHRhZyBvbiBNYXN0b2RvbiBvciBhbm90aGVyIHBvcHVsYXIgcGxhdGZvcm0uDQoNCioqQSBzb2x1dGlvbjoqKiB0cmFuc2Zvcm0gdG8gYSBoZXggc3RyaW5nIGFuZCBzaGlmdCB0aGUgMC4uZiB1cCB0byBhLi5wIHNvIGFsbCB0aGUgYnl0ZXMgYXJlIGxvd2VyY2FzZSBMYXRpbiBjaGFyYWN0ZXJzLg0KDQoNCkZpcnN0LCBsb2FkIHVwIHt0aWR5dmVyc2V9Og0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpgYGANCg0KDQpJIHdhbnQgdG8gdHJhbnNmb3JtIGEgc3RyaW5nIGxpa2UgIkhlbGxvIiB0byBhIGhleCBzdHJpbmc6DQoNCmBgYHtyfQ0KY2hhcnMyaGV4c3RyIDwtIGZ1bmN0aW9uKHQpIHsNCiAgdCB8Pg0KICAgIGNoYXJUb1JhdygpIHw+DQogICAgYXMuY2hhcmFjdGVyKCkgfD4NCiAgICBwYXN0ZShjb2xsYXBzZSA9ICIiKQ0KfQ0KYGBgDQoNClRlc3Rpbmc6DQoNCmBgYHtyfQ0KY2hhcnMyaGV4c3RyKCJIZWxsbyIpDQpgYGANCg0KQ29uc3VsdGluZyBhbiBbQVNDSUkgdGFibGVdKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0FTQ0lJI1ByaW50YWJsZV9jaGFyYWN0ZXJzKSwgbG9va3MgZ29vZC4gIkgiID0gMHg0OCwgImUiID0gMHg2NSwgImwiID0gMHg2YywgYW5kICJvIiA9IDB4NmYuDQoNCg0KTGV0J3MgdW5kbyBpdDoNCg0KYGBge3J9DQpoZXhzdHIyY2hhcnMgPC0gZnVuY3Rpb24oaGV4c3RyKSB7DQogIHN1YnN0cmluZyhoZXhzdHIsDQogICAgICAgICAgICBzZXEoMSwgbmNoYXIoaGV4c3RyKSAtIDEsIDIpLA0KICAgICAgICAgICAgc2VxKDIsIG5jaGFyKGhleHN0ciksIDIpKSB8Pg0KICBzdHJ0b2koYmFzZSA9IDE2KSB8Pg0KICBhcy5yYXcoKSB8Pg0KICByYXdUb0NoYXIoKQ0KfQ0KYGBgDQoNCg0KQW5kIGEgdGVzdDoNCg0KYGBge3J9DQpoZXhzdHIyY2hhcnMoIjQ4NjU2YzZjNmYiKQ0KYGBgDQoNCk5leHQsIEkgd2FudCB0byBzaGlmdCAwLi5mIHVwIHRvIGEuLnAuIEknbSBzdXJlIHRoZXJlJ3MgYW4gZWFzaWVyIHdheS4gSGVyZSdzIGhvdyBJJ20gZG9pbmcgaXQuIEZpcnN0LCBzZXR1cCBhIG1hcHBpbmc6DQoNCmBgYHtyfQ0KY2hhcm1hcCA8LSBkYXRhLmZyYW1lKA0KICBoZXggPSBjKDA6OSwgbGV0dGVyc1sxOjZdKSwNCiAgbGV0ID0gbGV0dGVyc1sxOjE2XSkNCmNoYXJtYXANCmBgYA0KDQpUaGVzZSB0d28gZnVuY3Rpb25zIHRyYW5zbGF0ZSBiYWNrIGFuZCBmb3J0aCBiZXR3ZWVuIDAuLmYgKGhleCkgYW5kIGEuLnAgKEknbSBnb2luZyB0byBjYWxsIGl0IGEgbGV0aGV4KSwgdXNpbmcgYSBgc3Ryc3BsaXRgIGFuZCBgam9pbmA6DQoNCmBgYHtyfQ0KaGV4MmxldGhleCA8LSBmdW5jdGlvbihoc3RyKSB7DQogIGRhdGEuZnJhbWUoaGV4ID0gKGhzdHIgfD4gc3Ryc3BsaXQoIiIpKVtbMV1dKSB8Pg0KICAgIGxlZnRfam9pbihjaGFybWFwLCBieSA9ICJoZXgiKSB8Pg0KICAgIHB1bGwobGV0KSB8Pg0KICAgIHBhc3RlKGNvbGxhcHNlID0gIiIpDQp9DQoNCmxldGhleDJoZXggPC0gZnVuY3Rpb24obGV0aGV4KSB7DQogIGRhdGEuZnJhbWUobGV0ID0gKGxldGhleCB8PiBzdHJzcGxpdCgiIikpW1sxXV0pIHw+DQogICAgbGVmdF9qb2luKGNoYXJtYXAsIGJ5ID0gImxldCIpIHw+DQogICAgcHVsbChoZXgpIHw+DQogICAgcGFzdGUoY29sbGFwc2UgPSAiIikNCn0NCmBgYA0KDQpUcnkgaXQgd2l0aCAiSGVsbG8iLiBIZXJlJ3MgdGhlIGhleCBzdHJpbmc6DQoNCmBgYHtyfQ0KaGVsbG9faGV4IDwtIGNoYXJzMmhleHN0cigiSGVsbG8iKQ0KaGVsbG9faGV4DQpgYGANCk5vdyBhcyBhIGxldGhleDoNCg0KYGBge3J9DQpoZWxsb19sZXRoZXggPC0gaGVsbG9faGV4IHw+IGhleDJsZXRoZXgoKQ0KaGVsbG9fbGV0aGV4DQpgYGANCg0KQW5kIGJhY2sgYWdhaW46DQoNCmBgYHtyfQ0KaGVsbG9fbGV0aGV4IHw+IGxldGhleDJoZXgoKQ0KYGBgDQoNCkZpbmFsbHksIGEgY291cGxlIG9mIHdyYXBwZXJzLCB3aGljaCB0YWtlIGEgc3RyaW5nIHRvIGEgbGV0aGV4IHN0cmluZyBhbmQgYmFjayBhZ2FpbjoNCg0KYGBge3J9DQpzdHIyaGFzaHRhZyA8LSBmdW5jdGlvbihzKSB7DQogIHMgfD4gY2hhcnMyaGV4c3RyKCkgfD4gaGV4MmxldGhleCgpDQp9DQoNCmhhc2h0YWcyc3RyIDwtIGZ1bmN0aW9uKGgpIHsNCiAgaCB8PiBsZXRoZXgyaGV4KCkgfD4gaGV4c3RyMmNoYXJzKCkNCn0NCmBgYA0KDQoNCmBgYHtyfQ0Kc3RyMmhhc2h0YWcoIkhlbGxvIikNCmBgYA0KDQpgYGB7cn0NCmhhc2h0YWcyc3RyKCJlaWdmZ21nbWdwIikNCmBgYA0KDQpGaW5hbGx5Og0KDQpgYGB7ciBldmFsPUZBTFNFfQ0KaGFzaHRhZzJzdHIoImdpaGVoZWhhaGRka2NwY3BoaGhoaGhjb2hqZ3BoZmhlaGZnY2dmY29nZGdwZ25jcGhoZ2JoZWdkZ2lkcGhnZG5nZWZiaGhkZWhoZGpmaGdoZmlnZGZiIikNCmBgYA0K