From e37d3b1a6e7d70214c39385cc36cabe9fd2b3c41 Mon Sep 17 00:00:00 2001 From: ChomeNS <95471003+ChomeNS@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:56:52 +0700 Subject: [PATCH] feat: support ascii85 in `*music playitem` --- build-number.txt | 2 +- .../chomens_bot/commands/MusicCommand.java | 18 +- .../chayapak1/chomens_bot/util/Ascii85.java | 190 ++++++++++++++++++ 3 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 src/main/java/me/chayapak1/chomens_bot/util/Ascii85.java diff --git a/build-number.txt b/build-number.txt index 3c17c81a..5a8694e0 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -2125 \ No newline at end of file +2129 \ No newline at end of file diff --git a/src/main/java/me/chayapak1/chomens_bot/commands/MusicCommand.java b/src/main/java/me/chayapak1/chomens_bot/commands/MusicCommand.java index 52f884a8..ca9a787e 100644 --- a/src/main/java/me/chayapak1/chomens_bot/commands/MusicCommand.java +++ b/src/main/java/me/chayapak1/chomens_bot/commands/MusicCommand.java @@ -7,6 +7,7 @@ import me.chayapak1.chomens_bot.song.Instrument; import me.chayapak1.chomens_bot.song.Loop; import me.chayapak1.chomens_bot.song.Note; import me.chayapak1.chomens_bot.song.Song; +import me.chayapak1.chomens_bot.util.Ascii85; import me.chayapak1.chomens_bot.util.ColorUtilities; import me.chayapak1.chomens_bot.util.PathUtilities; import me.chayapak1.chomens_bot.util.TimestampUtilities; @@ -91,11 +92,11 @@ public class MusicCommand extends Command { if (player.loaderThread != null) throw new CommandException(Component.text("Already loading a song")); - String stringPath; - Path path; - try { - stringPath = context.getString(true, true); + final String stringPath = context.getString(true, true); + Path path; + + try { path = Path.of(ROOT.toString(), stringPath); if (path.toString().contains("http")) player.loadSong(new URI(stringPath).toURL(), context.sender); @@ -196,7 +197,14 @@ public class MusicCommand extends Command { context.sender ); } catch (IllegalArgumentException e) { - context.sendOutput(Component.text("Invalid base64 in the selected item").color(NamedTextColor.RED)); + try { + bot.music.loadSong( + Ascii85.decode(output), + context.sender + ); + } catch (IllegalArgumentException e2) { + context.sendOutput(Component.text("Invalid Base64 or Ascii85 in the selected item").color(NamedTextColor.RED)); + } } return output; diff --git a/src/main/java/me/chayapak1/chomens_bot/util/Ascii85.java b/src/main/java/me/chayapak1/chomens_bot/util/Ascii85.java new file mode 100644 index 00000000..2221fcb5 --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/util/Ascii85.java @@ -0,0 +1,190 @@ +package me.chayapak1.chomens_bot.util; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.regex.Pattern; + +/** + * A very simple class that helps encode/decode for Ascii85 / base85 + * The version that is likely most similar that is implemented here would be the Adobe version. + *
+ * This code is from https://github.com/fzakaria/ascii85/blob/master/src/main/java/com/github/fzakaria/ascii85/Ascii85.java. Thank you! + * + * @see Ascii85 + */ +public class Ascii85 { + + private final static int ASCII_SHIFT = 33; + + private static final int[] BASE85_POW = { + 1, + 85, + 85 * 85, + 85 * 85 * 85, + 85 * 85 * 85 *85 + }; + + private static final Pattern REMOVE_WHITESPACE = Pattern.compile("\\s+"); + + private Ascii85 () {} + + public static String encode(byte[] payload) { + if (payload == null) { + throw new IllegalArgumentException("You must provide a non-null input"); + } + // By using five ASCII characters to represent four bytes of binary data the encoded size ¹⁄₄ is larger than the original + StringBuilder stringBuff = new StringBuilder(payload.length * 5/4); + // We break the payload into int (4 bytes) + byte[] chunk = new byte[4]; + int chunkIndex = 0; + for (byte currByte : payload) { + chunk[chunkIndex++] = currByte; + + if (chunkIndex == 4) { + int value = byteToInt(chunk); + //Because all-zero data is quite common, an exception is made for the sake of data compression, + //and an all-zero group is encoded as a single character "z" instead of "!!!!!". + if (value == 0) { + stringBuff.append('z'); + } else { + stringBuff.append(encodeChunk(value)); + } + Arrays.fill(chunk, (byte) 0); + chunkIndex = 0; + } + } + + //If we didn't end on 0, then we need some padding + if (chunkIndex > 0) { + int numPadded = chunk.length - chunkIndex; + Arrays.fill(chunk, chunkIndex, chunk.length, (byte)0); + int value = byteToInt(chunk); + char[] encodedChunk = encodeChunk(value); + for(int i = 0 ; i < encodedChunk.length - numPadded; i++) { + stringBuff.append(encodedChunk[i]); + } + } + + return stringBuff.toString(); + } + + private static char[] encodeChunk(int value) { + //transform value to unsigned long + long longValue = value & 0x00000000ffffffffL; + char[] encodedChunk = new char[5]; + for(int i = 0 ; i < encodedChunk.length; i++) { + encodedChunk[i] = (char) ((longValue / BASE85_POW[4 - i]) + ASCII_SHIFT); + longValue = longValue % BASE85_POW[4 - i]; + } + return encodedChunk; + } + + /** + * This is a very simple base85 decoder. It respects the 'z' optimization for empty chunks, and + * strips whitespace between characters to respect line limits. + * @see Ascii85 + * @param chars The input characters that are base85 encoded. + * @return The binary data decoded from the input + */ + public static byte[] decode(String chars) { + if (chars == null) { + throw new IllegalArgumentException("You must provide a non-null input"); + } + // Because we perform compression when encoding four bytes of zeros to a single 'z', we need + // to scan through the input to compute the target length, instead of just subtracting 20% of + // the encoded text length. + final int inputLength = chars.length(); + + // lets first count the occurrences of 'z' + long zCount = chars.chars().filter(c -> c == 'z').count(); + + // Typically by using five ASCII characters to represent four bytes of binary data + // the encoded size ¹⁄₄ is larger than the original. + // We however have to account for the 'z' which were compressed + BigDecimal uncompressedZLength = BigDecimal.valueOf(zCount).multiply(BigDecimal.valueOf(4)); + + BigDecimal uncompressedNonZLength = BigDecimal.valueOf(inputLength - zCount) + .multiply(BigDecimal.valueOf(4)) + .divide(BigDecimal.valueOf(5)); + + BigDecimal uncompressedLength = uncompressedZLength.add(uncompressedNonZLength); + + ByteBuffer bytebuff = ByteBuffer.allocate(uncompressedLength.intValue()); + //1. Whitespace characters may occur anywhere to accommodate line length limitations. So lets strip it. + chars = REMOVE_WHITESPACE.matcher(chars).replaceAll(""); + //Since Base85 is an ascii encoder, we don't need to get the bytes as UTF-8. + byte[] payload = chars.getBytes(StandardCharsets.US_ASCII); + byte[] chunk = new byte[5]; + int chunkIndex = 0; + + for (byte currByte : payload) { + // Because all-zero data is quite common, an exception is made for the sake of data compression, + // and an all-zero group is encoded as a single character "z" instead of "!!!!!". + if (currByte == 'z') { + if (chunkIndex > 0) { + throw new IllegalArgumentException("The payload is not base 85 encoded."); + } + chunk[chunkIndex++] = '!'; + chunk[chunkIndex++] = '!'; + chunk[chunkIndex++] = '!'; + chunk[chunkIndex++] = '!'; + chunk[chunkIndex++] = '!'; + } else { + chunk[chunkIndex++] = currByte; + } + + if (chunkIndex == 5) { + bytebuff.put(decodeChunk(chunk)); + Arrays.fill(chunk, (byte) 0); + chunkIndex = 0; + } + } + + // If we didn't end on 0, then we need some padding + if (chunkIndex > 0) { + int numPadded = chunk.length - chunkIndex; + Arrays.fill(chunk, chunkIndex, chunk.length, (byte)'u'); + byte[] paddedDecode = decodeChunk(chunk); + for(int i = 0 ; i < paddedDecode.length - numPadded; i++) { + bytebuff.put(paddedDecode[i]); + } + } + + bytebuff.flip(); + return Arrays.copyOf(bytebuff.array(),bytebuff.limit()); + } + + private static byte[] decodeChunk(byte[] chunk) { + if (chunk.length != 5) { + throw new IllegalArgumentException("You can only decode chunks of size 5."); + } + + int value = 0; + + value += (chunk[0] - ASCII_SHIFT) * BASE85_POW[4]; + value += (chunk[1] - ASCII_SHIFT) * BASE85_POW[3]; + value += (chunk[2] - ASCII_SHIFT) * BASE85_POW[2]; + value += (chunk[3] - ASCII_SHIFT) * BASE85_POW[1]; + value += (chunk[4] - ASCII_SHIFT) * BASE85_POW[0]; + + return intToByte(value); + } + + private static int byteToInt(byte[] value) { + if (value == null || value.length != 4) { + throw new IllegalArgumentException("You cannot create an int without exactly 4 bytes."); + } + return ByteBuffer.wrap(value).getInt(); + } + + private static byte[] intToByte(int value) { + return new byte[] { + (byte) (value >>> 24), + (byte) (value >>> 16), + (byte) (value >>> 8), + (byte) (value) + }; + } +}