Problem: you want to encode arbitrary binary data as a hashtag on Mastodon.

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")
LS0tDQp0aXRsZTogIkVuY29kZSBieXRlcyBhcyBoYXNodGFncyINCmF1dGhvcjogIkBhbmRpQHRlY2gubGdidCINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIGNvZGVfZm9sZGluZzogbm9uZQ0KLS0tDQoNCioqUHJvYmxlbToqKiB5b3Ugd2FudCB0byBlbmNvZGUgYXJiaXRyYXJ5IGJpbmFyeSBkYXRhIGFzIGEgaGFzaHRhZyBvbiBNYXN0b2Rvbi4NCg0KKipBIHNvbHV0aW9uOioqIHRyYW5zZm9ybSB0byBhIGhleCBzdHJpbmcgYW5kIHNoaWZ0IHRoZSAwLi5mIHVwIHRvIGEuLnAgc28gYWxsIHRoZSBieXRlcyBhcmUgbG93ZXJjYXNlIExhdGluIGNoYXJhY3RlcnMuDQoNCg0KRmlyc3QsIGxvYWQgdXAge3RpZHl2ZXJzZX06DQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmBgYA0KDQoNCkkgd2FudCB0byB0cmFuc2Zvcm0gYSBzdHJpbmcgbGlrZSAiSGVsbG8iIHRvIGEgaGV4IHN0cmluZzoNCg0KYGBge3J9DQpjaGFyczJoZXhzdHIgPC0gZnVuY3Rpb24odCkgew0KICB0IHw+DQogICAgY2hhclRvUmF3KCkgfD4NCiAgICBhcy5jaGFyYWN0ZXIoKSB8Pg0KICAgIHBhc3RlKGNvbGxhcHNlID0gIiIpDQp9DQpgYGANCg0KVGVzdGluZzoNCg0KYGBge3J9DQpjaGFyczJoZXhzdHIoIkhlbGxvIikNCmBgYA0KDQpDb25zdWx0aW5nIGFuIFtBU0NJSSB0YWJsZV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvQVNDSUkjUHJpbnRhYmxlX2NoYXJhY3RlcnMpLCBsb29rcyBnb29kLiAiSCIgPSAweDQ4LCAiZSIgPSAweDY1LCAibCIgPSAweDZjLCBhbmQgIm8iID0gMHg2Zi4NCg0KDQpMZXQncyB1bmRvIGl0Og0KDQpgYGB7cn0NCmhleHN0cjJjaGFycyA8LSBmdW5jdGlvbihoZXhzdHIpIHsNCiAgc3Vic3RyaW5nKGhleHN0ciwNCiAgICAgICAgICAgIHNlcSgxLCBuY2hhcihoZXhzdHIpIC0gMSwgMiksDQogICAgICAgICAgICBzZXEoMiwgbmNoYXIoaGV4c3RyKSwgMikpIHw+DQogIHN0cnRvaShiYXNlID0gMTYpIHw+DQogIGFzLnJhdygpIHw+DQogIHJhd1RvQ2hhcigpDQp9DQpgYGANCg0KDQpBbmQgYSB0ZXN0Og0KDQpgYGB7cn0NCmhleHN0cjJjaGFycygiNDg2NTZjNmM2ZiIpDQpgYGANCg0KTmV4dCwgSSB3YW50IHRvIHNoaWZ0IDAuLmYgdXAgdG8gYS4ucC4gSSdtIHN1cmUgdGhlcmUncyBhbiBlYXNpZXIgd2F5LiBIZXJlJ3MgaG93IEknbSBkb2luZyBpdC4gRmlyc3QsIHNldHVwIGEgbWFwcGluZzoNCg0KYGBge3J9DQpjaGFybWFwIDwtIGRhdGEuZnJhbWUoDQogIGhleCA9IGMoMDo5LCBsZXR0ZXJzWzE6Nl0pLA0KICBsZXQgPSBsZXR0ZXJzWzE6MTZdKQ0KY2hhcm1hcA0KYGBgDQoNClRoZXNlIHR3byBmdW5jdGlvbnMgdHJhbnNsYXRlIGJhY2sgYW5kIGZvcnRoIGJldHdlZW4gMC4uZiAoaGV4KSBhbmQgYS4ucCAoSSdtIGdvaW5nIHRvIGNhbGwgaXQgYSBsZXRoZXgpLCB1c2luZyBhIGBzdHJzcGxpdGAgYW5kIGBqb2luYDoNCg0KYGBge3J9DQpoZXgybGV0aGV4IDwtIGZ1bmN0aW9uKGhzdHIpIHsNCiAgZGF0YS5mcmFtZShoZXggPSAoaHN0ciB8PiBzdHJzcGxpdCgiIikpW1sxXV0pIHw+DQogICAgbGVmdF9qb2luKGNoYXJtYXAsIGJ5ID0gImhleCIpIHw+DQogICAgcHVsbChsZXQpIHw+DQogICAgcGFzdGUoY29sbGFwc2UgPSAiIikNCn0NCg0KbGV0aGV4MmhleCA8LSBmdW5jdGlvbihsZXRoZXgpIHsNCiAgZGF0YS5mcmFtZShsZXQgPSAobGV0aGV4IHw+IHN0cnNwbGl0KCIiKSlbWzFdXSkgfD4NCiAgICBsZWZ0X2pvaW4oY2hhcm1hcCwgYnkgPSAibGV0IikgfD4NCiAgICBwdWxsKGhleCkgfD4NCiAgICBwYXN0ZShjb2xsYXBzZSA9ICIiKQ0KfQ0KYGBgDQoNClRyeSBpdCB3aXRoICJIZWxsbyIuIEhlcmUncyB0aGUgaGV4IHN0cmluZzoNCg0KYGBge3J9DQpoZWxsb19oZXggPC0gY2hhcnMyaGV4c3RyKCJIZWxsbyIpDQpoZWxsb19oZXgNCmBgYA0KTm93IGFzIGEgbGV0aGV4Og0KDQpgYGB7cn0NCmhlbGxvX2xldGhleCA8LSBoZWxsb19oZXggfD4gaGV4MmxldGhleCgpDQpoZWxsb19sZXRoZXgNCmBgYA0KDQpBbmQgYmFjayBhZ2FpbjoNCg0KYGBge3J9DQpoZWxsb19sZXRoZXggfD4gbGV0aGV4MmhleCgpDQpgYGANCg0KRmluYWxseSwgYSBjb3VwbGUgb2Ygd3JhcHBlcnMsIHdoaWNoIHRha2UgYSBzdHJpbmcgdG8gYSBsZXRoZXggc3RyaW5nIGFuZCBiYWNrIGFnYWluOg0KDQpgYGB7cn0NCnN0cjJoYXNodGFnIDwtIGZ1bmN0aW9uKHMpIHsNCiAgcyB8PiBjaGFyczJoZXhzdHIoKSB8PiBoZXgybGV0aGV4KCkNCn0NCg0KaGFzaHRhZzJzdHIgPC0gZnVuY3Rpb24oaCkgew0KICBoIHw+IGxldGhleDJoZXgoKSB8PiBoZXhzdHIyY2hhcnMoKQ0KfQ0KYGBgDQoNCg0KYGBge3J9DQpzdHIyaGFzaHRhZygiSGVsbG8iKQ0KYGBgDQoNCmBgYHtyfQ0KaGFzaHRhZzJzdHIoImVpZ2ZnbWdtZ3AiKQ0KYGBgDQoNCkZpbmFsbHk6DQoNCmBgYHtyIGV2YWw9RkFMU0V9DQpoYXNodGFnMnN0cigiZ2loZWhlaGFoZGRrY3BjcGhoaGhoaGNvaGpncGhmaGVoZmdjZ2Zjb2dkZ3BnbmNwaGhnYmhlZ2RnaWRwaGdkbmdlZmJoaGRlaGhkamZoZ2hmaWdkZmIiKQ0KYGBgDQo=