View Javadoc
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  
12  import java.nio.charset.StandardCharsets;
13  import java.text.MessageFormat;
14  import java.util.Arrays;
15  
16  import org.eclipse.jgit.internal.JGitText;
17  
18  /**
19   * Base-85 encoder/decoder.
20   *
21   * @since 5.12
22   */
23  public final class Base85 {
24  
25  	private static final byte[] ENCODE = ("0123456789" //$NON-NLS-1$
26  			+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" //$NON-NLS-1$
27  			+ "abcdefghijklmnopqrstuvwxyz" //$NON-NLS-1$
28  			+ "!#$%&()*+-;<=>?@^_`{|}~") //$NON-NLS-1$
29  					.getBytes(StandardCharsets.US_ASCII);
30  
31  	private static final int[] DECODE = new int[256];
32  
33  	static {
34  		Arrays.fill(DECODE, -1);
35  		for (int i = 0; i < ENCODE.length; i++) {
36  			DECODE[ENCODE[i]] = i;
37  		}
38  	}
39  
40  	private Base85() {
41  		// No instantiation
42  	}
43  
44  	/**
45  	 * Determines the length of the base-85 encoding for {@code rawLength}
46  	 * bytes.
47  	 *
48  	 * @param rawLength
49  	 *            number of bytes to encode
50  	 * @return number of bytes needed for the base-85 encoding of
51  	 *         {@code rawLength} bytes
52  	 */
53  	public static int encodedLength(int rawLength) {
54  		return (rawLength + 3) / 4 * 5;
55  	}
56  
57  	/**
58  	 * Encodes the given {@code data} in Base-85.
59  	 *
60  	 * @param data
61  	 *            to encode
62  	 * @return encoded data
63  	 */
64  	public static byte[] encode(byte[] data) {
65  		return encode(data, 0, data.length);
66  	}
67  
68  	/**
69  	 * Encodes {@code length} bytes of {@code data} in Base-85, beginning at the
70  	 * {@code start} index.
71  	 *
72  	 * @param data
73  	 *            to encode
74  	 * @param start
75  	 *            index of the first byte to encode
76  	 * @param length
77  	 *            number of bytes to encode
78  	 * @return encoded data
79  	 */
80  	public static byte[] encode(byte[] data, int start, int length) {
81  		byte[] result = new byte[encodedLength(length)];
82  		int end = start + length;
83  		int in = start;
84  		int out = 0;
85  		while (in < end) {
86  			// Accumulate remaining bytes MSB first as a 32bit value
87  			long accumulator = ((long) (data[in++] & 0xFF)) << 24;
88  			if (in < end) {
89  				accumulator |= (data[in++] & 0xFF) << 16;
90  				if (in < end) {
91  					accumulator |= (data[in++] & 0xFF) << 8;
92  					if (in < end) {
93  						accumulator |= (data[in++] & 0xFF);
94  					}
95  				}
96  			}
97  			// Write the 32bit value in base-85 encoding, also MSB first
98  			for (int i = 4; i >= 0; i--) {
99  				result[out + i] = ENCODE[(int) (accumulator % 85)];
100 				accumulator /= 85;
101 			}
102 			out += 5;
103 		}
104 		return result;
105 	}
106 
107 	/**
108 	 * Decodes the Base-85 {@code encoded} data into a byte array of
109 	 * {@code expectedSize} bytes.
110 	 *
111 	 * @param encoded
112 	 *            Base-85 encoded data
113 	 * @param expectedSize
114 	 *            of the result
115 	 * @return the decoded bytes
116 	 * @throws IllegalArgumentException
117 	 *             if expectedSize doesn't match, the encoded data has a length
118 	 *             that is not a multiple of 5, or there are invalid characters
119 	 *             in the encoded data
120 	 */
121 	public static byte[] decode(byte[] encoded, int expectedSize) {
122 		return decode(encoded, 0, encoded.length, expectedSize);
123 	}
124 
125 	/**
126 	 * Decodes {@code length} bytes of Base-85 {@code encoded} data, beginning
127 	 * at the {@code start} index, into a byte array of {@code expectedSize}
128 	 * bytes.
129 	 *
130 	 * @param encoded
131 	 *            Base-85 encoded data
132 	 * @param start
133 	 *            index at which the data to decode starts in {@code encoded}
134 	 * @param length
135 	 *            of the Base-85 encoded data
136 	 * @param expectedSize
137 	 *            of the result
138 	 * @return the decoded bytes
139 	 * @throws IllegalArgumentException
140 	 *             if expectedSize doesn't match, {@code length} is not a
141 	 *             multiple of 5, or there are invalid characters in the encoded
142 	 *             data
143 	 */
144 	public static byte[] decode(byte[] encoded, int start, int length,
145 			int expectedSize) {
146 		if (length % 5 != 0) {
147 			throw new IllegalArgumentException(JGitText.get().base85length);
148 		}
149 		byte[] result = new byte[expectedSize];
150 		int end = start + length;
151 		int in = start;
152 		int out = 0;
153 		while (in < end && out < expectedSize) {
154 			// Accumulate 5 bytes, "MSB" first
155 			long accumulator = 0;
156 			for (int i = 4; i >= 0; i--) {
157 				int val = DECODE[encoded[in++] & 0xFF];
158 				if (val < 0) {
159 					throw new IllegalArgumentException(MessageFormat.format(
160 							JGitText.get().base85invalidChar,
161 							Integer.toHexString(encoded[in - 1] & 0xFF)));
162 				}
163 				accumulator = accumulator * 85 + val;
164 			}
165 			if (accumulator > 0xFFFF_FFFFL) {
166 				throw new IllegalArgumentException(
167 						MessageFormat.format(JGitText.get().base85overflow,
168 								Long.toHexString(accumulator)));
169 			}
170 			// Write remaining bytes, MSB first
171 			result[out++] = (byte) (accumulator >>> 24);
172 			if (out < expectedSize) {
173 				result[out++] = (byte) (accumulator >>> 16);
174 				if (out < expectedSize) {
175 					result[out++] = (byte) (accumulator >>> 8);
176 					if (out < expectedSize) {
177 						result[out++] = (byte) accumulator;
178 					}
179 				}
180 			}
181 		}
182 		// Should have exhausted 'in' and filled 'out' completely
183 		if (in < end) {
184 			throw new IllegalArgumentException(
185 					MessageFormat.format(JGitText.get().base85tooLong,
186 							Integer.valueOf(expectedSize)));
187 		}
188 		if (out < expectedSize) {
189 			throw new IllegalArgumentException(
190 					MessageFormat.format(JGitText.get().base85tooShort,
191 							Integer.valueOf(expectedSize)));
192 		}
193 		return result;
194 	}
195 }