diff --git a/pom.xml b/pom.xml index 0f2d436..187915d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,84 +1,85 @@ - - 4.0.0 + + 4.0.0 - com.github.multiformats - java-multibase - v1.2.0 - jar + com.github.multiformats + java-multibase + v1.3.0-SNAPSHOT + jar - multibase - https://github.com/multiformats/java-multibase + multibase + https://github.com/multiformats/java-multibase - - https://github.com/multiformats/java-multibase/issues - GitHub Issues - + + https://github.com/multiformats/java-multibase/issues + GitHub Issues + - - https://github.com/multiformats/java-multibase - scm:git:git://github.com/multiformats/java-multibase.git - scm:git:git@github.com:multiformats/java-multibase.git - + + https://github.com/multiformats/java-multibase + scm:git:git://github.com/multiformats/java-multibase.git + scm:git:git@github.com:multiformats/java-multibase.git + - - - MIT License - https://github.com/multiformats/java-multiaddr/blob/master/LICENSE - repo - - + + + MIT License + https://github.com/multiformats/java-multiaddr/blob/master/LICENSE + repo + + - - UTF-8 - UTF-8 - 5.11.0 - 3.0 - + + UTF-8 + UTF-8 + 5.11.0 + 3.0 + - - - org.junit.jupiter - junit-jupiter - ${version.junit} - test - - - org.hamcrest - hamcrest - ${version.hamcrest} - test - - + + + org.junit.jupiter + junit-jupiter + ${version.junit} + test + + + org.hamcrest + hamcrest + ${version.hamcrest} + test + + - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 11 - 11 - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.3.1 - - - org.apache.maven.plugins - maven-jar-plugin - 3.0.2 - - - - true - - - - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 11 + 11 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + + true + + + + + + diff --git a/src/main/java/io/ipfs/multibase/Base256Emoji.java b/src/main/java/io/ipfs/multibase/Base256Emoji.java new file mode 100644 index 0000000..8cc11e9 --- /dev/null +++ b/src/main/java/io/ipfs/multibase/Base256Emoji.java @@ -0,0 +1,103 @@ +package io.ipfs.multibase; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/* + * Copyright 2025 Michael Vorburger.ch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Base256Emoji + * is an encoding mapping each 0-255 byte value to (or from) a specific single Unicode Emoji character. + * + * @author Michael Vorburger.ch + */ +public class Base256Emoji { + + // from https://github.com/multiformats/multibase/blob/master/rfcs/Base256Emoji.md + private static final String[] EMOJIS = { + "๐Ÿš€", "๐Ÿช", "โ˜„", "๐Ÿ›ฐ", "๐ŸŒŒ", "๐ŸŒ‘", "๐ŸŒ’", "๐ŸŒ“", "๐ŸŒ”", "๐ŸŒ•", + "๐ŸŒ–", "๐ŸŒ—", "๐ŸŒ˜", "๐ŸŒ", "๐ŸŒ", "๐ŸŒŽ", "๐Ÿ‰", "โ˜€", "๐Ÿ’ป", "๐Ÿ–ฅ", + "๐Ÿ’พ", "๐Ÿ’ฟ", "๐Ÿ˜‚", "โค", "๐Ÿ˜", "๐Ÿคฃ", "๐Ÿ˜Š", "๐Ÿ™", "๐Ÿ’•", "๐Ÿ˜ญ", + "๐Ÿ˜˜", "๐Ÿ‘", "๐Ÿ˜…", "๐Ÿ‘", "๐Ÿ˜", "๐Ÿ”ฅ", "๐Ÿฅฐ", "๐Ÿ’”", "๐Ÿ’–", "๐Ÿ’™", + "๐Ÿ˜ข", "๐Ÿค”", "๐Ÿ˜†", "๐Ÿ™„", "๐Ÿ’ช", "๐Ÿ˜‰", "โ˜บ", "๐Ÿ‘Œ", "๐Ÿค—", "๐Ÿ’œ", + "๐Ÿ˜”", "๐Ÿ˜Ž", "๐Ÿ˜‡", "๐ŸŒน", "๐Ÿคฆ", "๐ŸŽ‰", "๐Ÿ’ž", "โœŒ", "โœจ", "๐Ÿคท", + "๐Ÿ˜ฑ", "๐Ÿ˜Œ", "๐ŸŒธ", "๐Ÿ™Œ", "๐Ÿ˜‹", "๐Ÿ’—", "๐Ÿ’š", "๐Ÿ˜", "๐Ÿ’›", "๐Ÿ™‚", + "๐Ÿ’“", "๐Ÿคฉ", "๐Ÿ˜„", "๐Ÿ˜€", "๐Ÿ–ค", "๐Ÿ˜ƒ", "๐Ÿ’ฏ", "๐Ÿ™ˆ", "๐Ÿ‘‡", "๐ŸŽถ", + "๐Ÿ˜’", "๐Ÿคญ", "โฃ", "๐Ÿ˜œ", "๐Ÿ’‹", "๐Ÿ‘€", "๐Ÿ˜ช", "๐Ÿ˜‘", "๐Ÿ’ฅ", "๐Ÿ™‹", + "๐Ÿ˜ž", "๐Ÿ˜ฉ", "๐Ÿ˜ก", "๐Ÿคช", "๐Ÿ‘Š", "๐Ÿฅณ", "๐Ÿ˜ฅ", "๐Ÿคค", "๐Ÿ‘‰", "๐Ÿ’ƒ", + "๐Ÿ˜ณ", "โœ‹", "๐Ÿ˜š", "๐Ÿ˜", "๐Ÿ˜ด", "๐ŸŒŸ", "๐Ÿ˜ฌ", "๐Ÿ™ƒ", "๐Ÿ€", "๐ŸŒท", + "๐Ÿ˜ป", "๐Ÿ˜“", "โญ", "โœ…", "๐Ÿฅบ", "๐ŸŒˆ", "๐Ÿ˜ˆ", "๐Ÿค˜", "๐Ÿ’ฆ", "โœ”", + "๐Ÿ˜ฃ", "๐Ÿƒ", "๐Ÿ’", "โ˜น", "๐ŸŽŠ", "๐Ÿ’˜", "๐Ÿ˜ ", "โ˜", "๐Ÿ˜•", "๐ŸŒบ", + "๐ŸŽ‚", "๐ŸŒป", "๐Ÿ˜", "๐Ÿ–•", "๐Ÿ’", "๐Ÿ™Š", "๐Ÿ˜น", "๐Ÿ—ฃ", "๐Ÿ’ซ", "๐Ÿ’€", + "๐Ÿ‘‘", "๐ŸŽต", "๐Ÿคž", "๐Ÿ˜›", "๐Ÿ”ด", "๐Ÿ˜ค", "๐ŸŒผ", "๐Ÿ˜ซ", "โšฝ", "๐Ÿค™", + "โ˜•", "๐Ÿ†", "๐Ÿคซ", "๐Ÿ‘ˆ", "๐Ÿ˜ฎ", "๐Ÿ™†", "๐Ÿป", "๐Ÿƒ", "๐Ÿถ", "๐Ÿ’", + "๐Ÿ˜ฒ", "๐ŸŒฟ", "๐Ÿงก", "๐ŸŽ", "โšก", "๐ŸŒž", "๐ŸŽˆ", "โŒ", "โœŠ", "๐Ÿ‘‹", + "๐Ÿ˜ฐ", "๐Ÿคจ", "๐Ÿ˜ถ", "๐Ÿค", "๐Ÿšถ", "๐Ÿ’ฐ", "๐Ÿ“", "๐Ÿ’ข", "๐ŸคŸ", "๐Ÿ™", + "๐Ÿšจ", "๐Ÿ’จ", "๐Ÿคฌ", "โœˆ", "๐ŸŽ€", "๐Ÿบ", "๐Ÿค“", "๐Ÿ˜™", "๐Ÿ’Ÿ", "๐ŸŒฑ", + "๐Ÿ˜–", "๐Ÿ‘ถ", "๐Ÿฅด", "โ–ถ", "โžก", "โ“", "๐Ÿ’Ž", "๐Ÿ’ธ", "โฌ‡", "๐Ÿ˜จ", + "๐ŸŒš", "๐Ÿฆ‹", "๐Ÿ˜ท", "๐Ÿ•บ", "โš ", "๐Ÿ™…", "๐Ÿ˜Ÿ", "๐Ÿ˜ต", "๐Ÿ‘Ž", "๐Ÿคฒ", + "๐Ÿค ", "๐Ÿคง", "๐Ÿ“Œ", "๐Ÿ”ต", "๐Ÿ’…", "๐Ÿง", "๐Ÿพ", "๐Ÿ’", "๐Ÿ˜—", "๐Ÿค‘", + "๐ŸŒŠ", "๐Ÿคฏ", "๐Ÿท", "โ˜Ž", "๐Ÿ’ง", "๐Ÿ˜ฏ", "๐Ÿ’†", "๐Ÿ‘†", "๐ŸŽค", "๐Ÿ™‡", + "๐Ÿ‘", "โ„", "๐ŸŒด", "๐Ÿ’ฃ", "๐Ÿธ", "๐Ÿ’Œ", "๐Ÿ“", "๐Ÿฅ€", "๐Ÿคข", "๐Ÿ‘…", + "๐Ÿ’ก", "๐Ÿ’ฉ", "๐Ÿ‘", "๐Ÿ“ธ", "๐Ÿ‘ป", "๐Ÿค", "๐Ÿคฎ", "๐ŸŽผ", "๐Ÿฅต", "๐Ÿšฉ", + "๐ŸŽ", "๐ŸŠ", "๐Ÿ‘ผ", "๐Ÿ’", "๐Ÿ“ฃ", "๐Ÿฅ‚" }; + + // TODO Propose adding a Guava dependency to use ImmutableMap instead of this + + private static final Map EMOJI_TO_INDEX; + private static final int MAP_EXPECTED_SIZE = EMOJIS.length; + private static final float MAP_LOAD_FACTOR = 1.0f; + + static { + if (EMOJIS.length != 256) { + throw new IllegalStateException("EMOJIS.length must be 256, but is " + EMOJIS.length); + } + + Map mutableMap = new HashMap<>(MAP_EXPECTED_SIZE, MAP_LOAD_FACTOR); + for (int i = 0; i < EMOJIS.length; i++) { + mutableMap.put(EMOJIS[i], i); + } + EMOJI_TO_INDEX = Collections.unmodifiableMap(mutableMap); + } + + public static String encode(byte[] in) { + StringBuilder sb = new StringBuilder(in.length); + for (byte b : in) { + sb.append(EMOJIS[b & 0xFF]); + } + return sb.toString(); + } + + public static byte[] decode(String in) { + int length = in.codePointCount(0, in.length()); + byte[] bytes = new byte[length]; + + for (int i = 0; i < in.codePointCount(0, in.length()); i++) { + int cp = in.codePointAt(in.offsetByCodePoints(0, i)); + String emoji = new String(Character.toChars(cp)); + Integer index = EMOJI_TO_INDEX.get(emoji); + if (index == null) { + throw new IllegalArgumentException("Unknown Base256Emoji character: " + emoji); + } + bytes[i] = (byte) (index & 0xFF); + } + + return bytes; + } + +} diff --git a/src/main/java/io/ipfs/multibase/Multibase.java b/src/main/java/io/ipfs/multibase/Multibase.java index bf54307..70369d7 100644 --- a/src/main/java/io/ipfs/multibase/Multibase.java +++ b/src/main/java/io/ipfs/multibase/Multibase.java @@ -1,52 +1,58 @@ package io.ipfs.multibase; -import io.ipfs.multibase.binary.*; -import io.ipfs.multibase.binary.Base64; +import java.util.Map; +import java.util.TreeMap; -import java.util.*; +import io.ipfs.multibase.binary.Base32; +import io.ipfs.multibase.binary.Base64; public class Multibase { public enum Base { - Base1('1'), - Base2('0'), - Base8('7'), - Base10('9'), - Base16('f'), - Base16Upper('F'), - Base32('b'), - Base32Upper('B'), - Base32Pad('c'), - Base32PadUpper('C'), - Base32Hex('v'), - Base32HexUpper('V'), - Base32HexPad('t'), - Base32HexPadUpper('T'), - Base36('k'), - Base36Upper('K'), - Base58BTC('z'), - Base58Flickr('Z'), - Base64('m'), - Base64Url('u'), - Base64Pad('M'), - Base64UrlPad('U'); + Base1("1"), + Base2("0"), + Base8("7"), + Base10("9"), + Base16("f"), + Base16Upper("F"), + Base32("b"), + Base32Upper("B"), + Base32Pad("c"), + Base32PadUpper("C"), + Base32Hex("v"), + Base32HexUpper("V"), + Base32HexPad("t"), + Base32HexPadUpper("T"), + Base36("k"), + Base36Upper("K"), + Base58BTC("z"), + Base58Flickr("Z"), + Base64("m"), + Base64Url("u"), + Base64Pad("M"), + Base64UrlPad("U"), + Base256Emoji("๐Ÿš€"); - public char prefix; + public String prefix; - Base(char prefix) { + Base(String prefix) { this.prefix = prefix; } - private static Map lookup = new TreeMap<>(); + private static Map lookup = new TreeMap<>(); static { - for (Base b: Base.values()) + for (Base b : Base.values()) lookup.put(b.prefix, b); } - public static Base lookup(char p) { - if (!lookup.containsKey(p)) - throw new IllegalArgumentException("Unknown Multibase type: " + p); - return lookup.get(p); + public static Base lookup(String data) { + String p = Character.toString(data.codePointAt(0)); + Base base = lookup.get(p); + if (base != null) + return base; + if (data.startsWith(Base256Emoji.prefix)) + return Base256Emoji; + throw new IllegalArgumentException("Unknown Multibase type: " + p); } } @@ -86,21 +92,23 @@ public static String encode(Base b, byte[] data) { return b.prefix + Base64.encodeBase64String(data); case Base64UrlPad: return b.prefix + Base64.encodeBase64String(data).replaceAll("\\+", "-").replaceAll("/", "_"); + case Base256Emoji: + return b.prefix + Base256Emoji.encode(data); default: throw new UnsupportedOperationException("Unsupported base encoding: " + b.name()); } } public static Base encoding(String data) { - return Base.lookup(data.charAt(0)); + return Base.lookup(data); } public static byte[] decode(String data) { - if(data.isEmpty()) { + if (data.isEmpty()) { throw new IllegalArgumentException("Cannot decode an empty string"); } Base b = encoding(data); - String rest = data.substring(1); + String rest = safeSubstringFromIndexOne(data); switch (b) { case Base58BTC: return Base58.decode(rest); @@ -129,8 +137,21 @@ public static byte[] decode(String data) { case Base64Pad: case Base64UrlPad: return Base64.decodeBase64(rest); + case Base256Emoji: + return Base256Emoji.decode(rest); default: throw new UnsupportedOperationException("Unsupported base encoding: " + b.name()); } } + + private static String safeSubstringFromIndexOne(String data) { + // Check if there's at least 2 code points in the string + if (data.codePointCount(0, data.length()) <= 1) { + return ""; + } + + // If so, do an Emoji-safe data.substring(1) equivalent: + int charIndex = data.offsetByCodePoints(0, 1); + return data.substring(charIndex); + } } diff --git a/src/test/java/io/ipfs/multibase/MultibaseBadInputsTest.java b/src/test/java/io/ipfs/multibase/MultibaseBadInputsTest.java index 93d0cd0..0c33db4 100755 --- a/src/test/java/io/ipfs/multibase/MultibaseBadInputsTest.java +++ b/src/test/java/io/ipfs/multibase/MultibaseBadInputsTest.java @@ -1,22 +1,23 @@ package io.ipfs.multibase; +import static org.junit.jupiter.api.Assertions.assertThrows; + import java.util.Arrays; import java.util.Collection; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import static org.junit.jupiter.api.Assertions.assertThrows; - public class MultibaseBadInputsTest { public static Collection data() { return Arrays.asList( - "f012", // Hex string of odd length, not allowed in Base16 - "f0g", // 'g' char is not allowed in Base16 - "zt1Zv2yaI", // 'I' char is not allowed in Base58 - "2", // '2' is not a valid encoding marker - "" // Empty string is not a valid multibase + "f012", // Hex string of odd length, not allowed in Base16 + "f0g", // 'g' char is not allowed in Base16 + "zt1Zv2yaI", // 'I' char is not allowed in Base58 + "2", // '2' is not a valid encoding marker + "", // Empty string is not a valid multibase + "๐Ÿš€๐Ÿซ•" // This Emoji (Swiss Fondue) is not part of the Base256Emoji table ); } diff --git a/src/test/java/io/ipfs/multibase/MultibaseTest.java b/src/test/java/io/ipfs/multibase/MultibaseTest.java index dbcd4fc..133f7b6 100755 --- a/src/test/java/io/ipfs/multibase/MultibaseTest.java +++ b/src/test/java/io/ipfs/multibase/MultibaseTest.java @@ -47,6 +47,9 @@ public static Collection data() { {Multibase.Base.Base64Url, hexToBytes("446563656e7472616c697a652065766572797468696e67212121"), "uRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE"}, {Multibase.Base.Base64Pad, hexToBytes("446563656e7472616c697a652065766572797468696e67212121"), "MRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE="}, {Multibase.Base.Base64UrlPad, hexToBytes("446563656e7472616c697a652065766572797468696e67212121"), "URGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE="}, + + {Multibase.Base.Base256Emoji, hexToBytes(""), "๐Ÿš€"}, + {Multibase.Base.Base256Emoji, hexToBytes("0107FF"), "๐Ÿš€๐Ÿช๐ŸŒ“๐Ÿฅ‚"}, }); }