|
|
@@ -0,0 +1,195 @@ |
|
|
|
/* |
|
|
|
* 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; |
|
|
|
} |
|
|
|
} |