blob: 54b7cfcaa71d1b4580a1c75ad1462b81ae0e30cd [file] [log] [blame]
/*
* Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.util;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Arrays;
import org.eclipse.jgit.internal.JGitText;
/**
* Base-85 encoder/decoder.
*
* @since 5.12
*/
public final class Base85 {
private static final byte[] ENCODE = ("0123456789" //$NON-NLS-1$
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" //$NON-NLS-1$
+ "abcdefghijklmnopqrstuvwxyz" //$NON-NLS-1$
+ "!#$%&()*+-;<=>?@^_`{|}~") //$NON-NLS-1$
.getBytes(StandardCharsets.US_ASCII);
private static final int[] DECODE = new int[256];
static {
Arrays.fill(DECODE, -1);
for (int i = 0; i < ENCODE.length; i++) {
DECODE[ENCODE[i]] = i;
}
}
private Base85() {
// No instantiation
}
/**
* Determines the length of the base-85 encoding for {@code rawLength}
* bytes.
*
* @param rawLength
* number of bytes to encode
* @return number of bytes needed for the base-85 encoding of
* {@code rawLength} bytes
*/
public static int encodedLength(int rawLength) {
return (rawLength + 3) / 4 * 5;
}
/**
* Encodes the given {@code data} in Base-85.
*
* @param data
* to encode
* @return encoded data
*/
public static byte[] encode(byte[] data) {
return encode(data, 0, data.length);
}
/**
* Encodes {@code length} bytes of {@code data} in Base-85, beginning at the
* {@code start} index.
*
* @param data
* to encode
* @param start
* index of the first byte to encode
* @param length
* number of bytes to encode
* @return encoded data
*/
public static byte[] encode(byte[] data, int start, int length) {
byte[] result = new byte[encodedLength(length)];
int end = start + length;
int in = start;
int out = 0;
while (in < end) {
// Accumulate remaining bytes MSB first as a 32bit value
long accumulator = ((long) (data[in++] & 0xFF)) << 24;
if (in < end) {
accumulator |= (data[in++] & 0xFF) << 16;
if (in < end) {
accumulator |= (data[in++] & 0xFF) << 8;
if (in < end) {
accumulator |= (data[in++] & 0xFF);
}
}
}
// Write the 32bit value in base-85 encoding, also MSB first
for (int i = 4; i >= 0; i--) {
result[out + i] = ENCODE[(int) (accumulator % 85)];
accumulator /= 85;
}
out += 5;
}
return result;
}
/**
* Decodes the Base-85 {@code encoded} data into a byte array of
* {@code expectedSize} bytes.
*
* @param encoded
* Base-85 encoded data
* @param expectedSize
* of the result
* @return the decoded bytes
* @throws IllegalArgumentException
* if expectedSize doesn't match, the encoded data has a length
* that is not a multiple of 5, or there are invalid characters
* in the encoded data
*/
public static byte[] decode(byte[] encoded, int expectedSize) {
return decode(encoded, 0, encoded.length, expectedSize);
}
/**
* Decodes {@code length} bytes of Base-85 {@code encoded} data, beginning
* at the {@code start} index, into a byte array of {@code expectedSize}
* bytes.
*
* @param encoded
* Base-85 encoded data
* @param start
* index at which the data to decode starts in {@code encoded}
* @param length
* of the Base-85 encoded data
* @param expectedSize
* of the result
* @return the decoded bytes
* @throws IllegalArgumentException
* if expectedSize doesn't match, {@code length} is not a
* multiple of 5, or there are invalid characters in the encoded
* data
*/
public static byte[] decode(byte[] encoded, int start, int length,
int expectedSize) {
if (length % 5 != 0) {
throw new IllegalArgumentException(JGitText.get().base85length);
}
byte[] result = new byte[expectedSize];
int end = start + length;
int in = start;
int out = 0;
while (in < end && out < expectedSize) {
// Accumulate 5 bytes, "MSB" first
long accumulator = 0;
for (int i = 4; i >= 0; i--) {
int val = DECODE[encoded[in++] & 0xFF];
if (val < 0) {
throw new IllegalArgumentException(MessageFormat.format(
JGitText.get().base85invalidChar,
Integer.toHexString(encoded[in - 1] & 0xFF)));
}
accumulator = accumulator * 85 + val;
}
if (accumulator > 0xFFFF_FFFFL) {
throw new IllegalArgumentException(
MessageFormat.format(JGitText.get().base85overflow,
Long.toHexString(accumulator)));
}
// Write remaining bytes, MSB first
result[out++] = (byte) (accumulator >>> 24);
if (out < expectedSize) {
result[out++] = (byte) (accumulator >>> 16);
if (out < expectedSize) {
result[out++] = (byte) (accumulator >>> 8);
if (out < expectedSize) {
result[out++] = (byte) accumulator;
}
}
}
}
// Should have exhausted 'in' and filled 'out' completely
if (in < end) {
throw new IllegalArgumentException(
MessageFormat.format(JGitText.get().base85tooLong,
Integer.valueOf(expectedSize)));
}
if (out < expectedSize) {
throw new IllegalArgumentException(
MessageFormat.format(JGitText.get().base85tooShort,
Integer.valueOf(expectedSize)));
}
return result;
}
}