Base85.java

  1. /*
  2.  * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.util;

  11. import java.nio.charset.StandardCharsets;
  12. import java.text.MessageFormat;
  13. import java.util.Arrays;

  14. import org.eclipse.jgit.internal.JGitText;

  15. /**
  16.  * Base-85 encoder/decoder.
  17.  *
  18.  * @since 5.12
  19.  */
  20. public final class Base85 {

  21.     private static final byte[] ENCODE = ("0123456789" //$NON-NLS-1$
  22.             + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" //$NON-NLS-1$
  23.             + "abcdefghijklmnopqrstuvwxyz" //$NON-NLS-1$
  24.             + "!#$%&()*+-;<=>?@^_`{|}~") //$NON-NLS-1$
  25.                     .getBytes(StandardCharsets.US_ASCII);

  26.     private static final int[] DECODE = new int[256];

  27.     static {
  28.         Arrays.fill(DECODE, -1);
  29.         for (int i = 0; i < ENCODE.length; i++) {
  30.             DECODE[ENCODE[i]] = i;
  31.         }
  32.     }

  33.     private Base85() {
  34.         // No instantiation
  35.     }

  36.     /**
  37.      * Determines the length of the base-85 encoding for {@code rawLength}
  38.      * bytes.
  39.      *
  40.      * @param rawLength
  41.      *            number of bytes to encode
  42.      * @return number of bytes needed for the base-85 encoding of
  43.      *         {@code rawLength} bytes
  44.      */
  45.     public static int encodedLength(int rawLength) {
  46.         return (rawLength + 3) / 4 * 5;
  47.     }

  48.     /**
  49.      * Encodes the given {@code data} in Base-85.
  50.      *
  51.      * @param data
  52.      *            to encode
  53.      * @return encoded data
  54.      */
  55.     public static byte[] encode(byte[] data) {
  56.         return encode(data, 0, data.length);
  57.     }

  58.     /**
  59.      * Encodes {@code length} bytes of {@code data} in Base-85, beginning at the
  60.      * {@code start} index.
  61.      *
  62.      * @param data
  63.      *            to encode
  64.      * @param start
  65.      *            index of the first byte to encode
  66.      * @param length
  67.      *            number of bytes to encode
  68.      * @return encoded data
  69.      */
  70.     public static byte[] encode(byte[] data, int start, int length) {
  71.         byte[] result = new byte[encodedLength(length)];
  72.         int end = start + length;
  73.         int in = start;
  74.         int out = 0;
  75.         while (in < end) {
  76.             // Accumulate remaining bytes MSB first as a 32bit value
  77.             long accumulator = ((long) (data[in++] & 0xFF)) << 24;
  78.             if (in < end) {
  79.                 accumulator |= (data[in++] & 0xFF) << 16;
  80.                 if (in < end) {
  81.                     accumulator |= (data[in++] & 0xFF) << 8;
  82.                     if (in < end) {
  83.                         accumulator |= (data[in++] & 0xFF);
  84.                     }
  85.                 }
  86.             }
  87.             // Write the 32bit value in base-85 encoding, also MSB first
  88.             for (int i = 4; i >= 0; i--) {
  89.                 result[out + i] = ENCODE[(int) (accumulator % 85)];
  90.                 accumulator /= 85;
  91.             }
  92.             out += 5;
  93.         }
  94.         return result;
  95.     }

  96.     /**
  97.      * Decodes the Base-85 {@code encoded} data into a byte array of
  98.      * {@code expectedSize} bytes.
  99.      *
  100.      * @param encoded
  101.      *            Base-85 encoded data
  102.      * @param expectedSize
  103.      *            of the result
  104.      * @return the decoded bytes
  105.      * @throws IllegalArgumentException
  106.      *             if expectedSize doesn't match, the encoded data has a length
  107.      *             that is not a multiple of 5, or there are invalid characters
  108.      *             in the encoded data
  109.      */
  110.     public static byte[] decode(byte[] encoded, int expectedSize) {
  111.         return decode(encoded, 0, encoded.length, expectedSize);
  112.     }

  113.     /**
  114.      * Decodes {@code length} bytes of Base-85 {@code encoded} data, beginning
  115.      * at the {@code start} index, into a byte array of {@code expectedSize}
  116.      * bytes.
  117.      *
  118.      * @param encoded
  119.      *            Base-85 encoded data
  120.      * @param start
  121.      *            index at which the data to decode starts in {@code encoded}
  122.      * @param length
  123.      *            of the Base-85 encoded data
  124.      * @param expectedSize
  125.      *            of the result
  126.      * @return the decoded bytes
  127.      * @throws IllegalArgumentException
  128.      *             if expectedSize doesn't match, {@code length} is not a
  129.      *             multiple of 5, or there are invalid characters in the encoded
  130.      *             data
  131.      */
  132.     public static byte[] decode(byte[] encoded, int start, int length,
  133.             int expectedSize) {
  134.         if (length % 5 != 0) {
  135.             throw new IllegalArgumentException(JGitText.get().base85length);
  136.         }
  137.         byte[] result = new byte[expectedSize];
  138.         int end = start + length;
  139.         int in = start;
  140.         int out = 0;
  141.         while (in < end && out < expectedSize) {
  142.             // Accumulate 5 bytes, "MSB" first
  143.             long accumulator = 0;
  144.             for (int i = 4; i >= 0; i--) {
  145.                 int val = DECODE[encoded[in++] & 0xFF];
  146.                 if (val < 0) {
  147.                     throw new IllegalArgumentException(MessageFormat.format(
  148.                             JGitText.get().base85invalidChar,
  149.                             Integer.toHexString(encoded[in - 1] & 0xFF)));
  150.                 }
  151.                 accumulator = accumulator * 85 + val;
  152.             }
  153.             if (accumulator > 0xFFFF_FFFFL) {
  154.                 throw new IllegalArgumentException(
  155.                         MessageFormat.format(JGitText.get().base85overflow,
  156.                                 Long.toHexString(accumulator)));
  157.             }
  158.             // Write remaining bytes, MSB first
  159.             result[out++] = (byte) (accumulator >>> 24);
  160.             if (out < expectedSize) {
  161.                 result[out++] = (byte) (accumulator >>> 16);
  162.                 if (out < expectedSize) {
  163.                     result[out++] = (byte) (accumulator >>> 8);
  164.                     if (out < expectedSize) {
  165.                         result[out++] = (byte) accumulator;
  166.                     }
  167.                 }
  168.             }
  169.         }
  170.         // Should have exhausted 'in' and filled 'out' completely
  171.         if (in < end) {
  172.             throw new IllegalArgumentException(
  173.                     MessageFormat.format(JGitText.get().base85tooLong,
  174.                             Integer.valueOf(expectedSize)));
  175.         }
  176.         if (out < expectedSize) {
  177.             throw new IllegalArgumentException(
  178.                     MessageFormat.format(JGitText.get().base85tooShort,
  179.                             Integer.valueOf(expectedSize)));
  180.         }
  181.         return result;
  182.     }
  183. }