Skip to content

Commit 166578b

Browse files
committed
🚀 Base256Emoji 😊
1 parent 7557151 commit 166578b

File tree

4 files changed

+133
-7
lines changed

4 files changed

+133
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.ipfs.multibase;
2+
3+
import java.util.Collections;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
7+
/*
8+
* Copyright 2025 Michael Vorburger.ch
9+
*
10+
* Licensed under the Apache License, Version 2.0 (the "License");
11+
* you may not use this file except in compliance with the License.
12+
* You may obtain a copy of the License at
13+
*
14+
* http://www.apache.org/licenses/LICENSE-2.0
15+
*
16+
* Unless required by applicable law or agreed to in writing, software
17+
* distributed under the License is distributed on an "AS IS" BASIS,
18+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19+
* See the License for the specific language governing permissions and
20+
* limitations under the License.
21+
*/
22+
23+
/**
24+
* <a href="https://github.com/multiformats/multibase/blob/master/rfcs/Base256Emoji.md">Base256Emoji</a>
25+
* is an encoding mapping each 0-255 byte value to (or from) a specific single Unicode Emoji character.
26+
*
27+
* @author <a href="https://www.vorburger.ch/">Michael Vorburger.ch</a>
28+
*/
29+
public class Base256Emoji {
30+
31+
// from https://github.com/multiformats/multibase/blob/master/rfcs/Base256Emoji.md
32+
private static final String[] EMOJIS = {
33+
"🚀", "🪐", "☄", "🛰", "🌌", "🌑", "🌒", "🌓", "🌔", "🌕",
34+
"🌖", "🌗", "🌘", "🌍", "🌏", "🌎", "🐉", "☀", "💻", "🖥",
35+
"💾", "💿", "😂", "❤", "😍", "🤣", "😊", "🙏", "💕", "😭",
36+
"😘", "👍", "😅", "👏", "😁", "🔥", "🥰", "💔", "💖", "💙",
37+
"😢", "🤔", "😆", "🙄", "💪", "😉", "☺", "👌", "🤗", "💜",
38+
"😔", "😎", "😇", "🌹", "🤦", "🎉", "💞", "✌", "✨", "🤷",
39+
"😱", "😌", "🌸", "🙌", "😋", "💗", "💚", "😏", "💛", "🙂",
40+
"💓", "🤩", "😄", "😀", "🖤", "😃", "💯", "🙈", "👇", "🎶",
41+
"😒", "🤭", "❣", "😜", "💋", "👀", "😪", "😑", "💥", "🙋",
42+
"😞", "😩", "😡", "🤪", "👊", "🥳", "😥", "🤤", "👉", "💃",
43+
"😳", "✋", "😚", "😝", "😴", "🌟", "😬", "🙃", "🍀", "🌷",
44+
"😻", "😓", "⭐", "✅", "🥺", "🌈", "😈", "🤘", "💦", "✔",
45+
"😣", "🏃", "💐", "☹", "🎊", "💘", "😠", "☝", "😕", "🌺",
46+
"🎂", "🌻", "😐", "🖕", "💝", "🙊", "😹", "🗣", "💫", "💀",
47+
"👑", "🎵", "🤞", "😛", "🔴", "😤", "🌼", "😫", "⚽", "🤙",
48+
"☕", "🏆", "🤫", "👈", "😮", "🙆", "🍻", "🍃", "🐶", "💁",
49+
"😲", "🌿", "🧡", "🎁", "⚡", "🌞", "🎈", "❌", "✊", "👋",
50+
"😰", "🤨", "😶", "🤝", "🚶", "💰", "🍓", "💢", "🤟", "🙁",
51+
"🚨", "💨", "🤬", "✈", "🎀", "🍺", "🤓", "😙", "💟", "🌱",
52+
"😖", "👶", "🥴", "▶", "➡", "❓", "💎", "💸", "⬇", "😨",
53+
"🌚", "🦋", "😷", "🕺", "⚠", "🙅", "😟", "😵", "👎", "🤲",
54+
"🤠", "🤧", "📌", "🔵", "💅", "🧐", "🐾", "🍒", "😗", "🤑",
55+
"🌊", "🤯", "🐷", "☎", "💧", "😯", "💆", "👆", "🎤", "🙇",
56+
"🍑", "❄", "🌴", "💣", "🐸", "💌", "📍", "🥀", "🤢", "👅",
57+
"💡", "💩", "👐", "📸", "👻", "🤐", "🤮", "🎼", "🥵", "🚩",
58+
"🍎", "🍊", "👼", "💍", "📣", "🥂" };
59+
60+
// TODO Propose adding a Guava dependency to use ImmutableMap instead of this
61+
62+
private static final Map<String, Integer> EMOJI_TO_INDEX;
63+
private static final int MAP_EXPECTED_SIZE = EMOJIS.length;
64+
private static final float MAP_LOAD_FACTOR = 1.0f;
65+
66+
static {
67+
if (EMOJIS.length != 256) {
68+
throw new IllegalStateException("EMOJIS.length must be 256, but is " + EMOJIS.length);
69+
}
70+
71+
Map<String, Integer> mutableMap = new HashMap<>(MAP_EXPECTED_SIZE, MAP_LOAD_FACTOR);
72+
for (int i = 0; i < EMOJIS.length; i++) {
73+
mutableMap.put(EMOJIS[i], i);
74+
}
75+
EMOJI_TO_INDEX = Collections.unmodifiableMap(mutableMap);
76+
}
77+
78+
public static String encode(byte[] in) {
79+
StringBuilder sb = new StringBuilder(in.length);
80+
for (byte b : in) {
81+
sb.append(EMOJIS[b & 0xFF]);
82+
}
83+
return sb.toString();
84+
}
85+
86+
public static byte[] decode(String in) {
87+
int length = in.codePointCount(0, in.length());
88+
byte[] bytes = new byte[length];
89+
90+
for (int i = 0; i < in.codePointCount(0, in.length()); i++) {
91+
int cp = in.codePointAt(in.offsetByCodePoints(0, i));
92+
String emoji = new String(Character.toChars(cp));
93+
Integer index = EMOJI_TO_INDEX.get(emoji);
94+
if (index == null) {
95+
throw new IllegalArgumentException("Unknown Base256Emoji character: " + emoji);
96+
}
97+
bytes[i] = (byte) (index & 0xFF);
98+
}
99+
100+
return bytes;
101+
}
102+
103+
}

src/main/java/io/ipfs/multibase/Multibase.java

+25-6
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public enum Base {
3030
Base64("m"),
3131
Base64Url("u"),
3232
Base64Pad("M"),
33-
Base64UrlPad("U");
33+
Base64UrlPad("U"),
34+
Base256Emoji("🚀");
3435

3536
public String prefix;
3637

@@ -45,10 +46,13 @@ public enum Base {
4546
}
4647

4748
public static Base lookup(String data) {
48-
String p = data.substring(0, 1);
49-
if (!lookup.containsKey(p))
50-
throw new IllegalArgumentException("Unknown Multibase type: " + p);
51-
return lookup.get(p);
49+
String p = Character.toString(data.codePointAt(0));
50+
Base base = lookup.get(p);
51+
if (base != null)
52+
return base;
53+
if (data.startsWith(Base256Emoji.prefix))
54+
return Base256Emoji;
55+
throw new IllegalArgumentException("Unknown Multibase type: " + p);
5256
}
5357
}
5458

@@ -88,6 +92,8 @@ public static String encode(Base b, byte[] data) {
8892
return b.prefix + Base64.encodeBase64String(data);
8993
case Base64UrlPad:
9094
return b.prefix + Base64.encodeBase64String(data).replaceAll("\\+", "-").replaceAll("/", "_");
95+
case Base256Emoji:
96+
return b.prefix + Base256Emoji.encode(data);
9197
default:
9298
throw new UnsupportedOperationException("Unsupported base encoding: " + b.name());
9399
}
@@ -102,7 +108,7 @@ public static byte[] decode(String data) {
102108
throw new IllegalArgumentException("Cannot decode an empty string");
103109
}
104110
Base b = encoding(data);
105-
String rest = data.substring(1);
111+
String rest = safeSubstringFromIndexOne(data);
106112
switch (b) {
107113
case Base58BTC:
108114
return Base58.decode(rest);
@@ -131,8 +137,21 @@ public static byte[] decode(String data) {
131137
case Base64Pad:
132138
case Base64UrlPad:
133139
return Base64.decodeBase64(rest);
140+
case Base256Emoji:
141+
return Base256Emoji.decode(rest);
134142
default:
135143
throw new UnsupportedOperationException("Unsupported base encoding: " + b.name());
136144
}
137145
}
146+
147+
private static String safeSubstringFromIndexOne(String data) {
148+
// Check if there's at least 2 code points in the string
149+
if (data.codePointCount(0, data.length()) <= 1) {
150+
return "";
151+
}
152+
153+
// If so, do an Emoji-safe data.substring(1) equivalent:
154+
int charIndex = data.offsetByCodePoints(0, 1);
155+
return data.substring(charIndex);
156+
}
138157
}

src/test/java/io/ipfs/multibase/MultibaseBadInputsTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public static Collection<String> data() {
1616
"f0g", // 'g' char is not allowed in Base16
1717
"zt1Zv2yaI", // 'I' char is not allowed in Base58
1818
"2", // '2' is not a valid encoding marker
19-
"" // Empty string is not a valid multibase
19+
"", // Empty string is not a valid multibase
20+
"🚀🫕" // This Emoji (Swiss Fondue) is not part of the Base256Emoji table
2021
);
2122
}
2223

src/test/java/io/ipfs/multibase/MultibaseTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ public static Collection<Object[]> data() {
4747
{Multibase.Base.Base64Url, hexToBytes("446563656e7472616c697a652065766572797468696e67212121"), "uRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE"},
4848
{Multibase.Base.Base64Pad, hexToBytes("446563656e7472616c697a652065766572797468696e67212121"), "MRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE="},
4949
{Multibase.Base.Base64UrlPad, hexToBytes("446563656e7472616c697a652065766572797468696e67212121"), "URGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE="},
50+
51+
{Multibase.Base.Base256Emoji, hexToBytes(""), "🚀"},
52+
{Multibase.Base.Base256Emoji, hexToBytes("0107FF"), "🚀🪐🌓🥂"},
5053
});
5154
}
5255

0 commit comments

Comments
 (0)