1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.eclipse.aether.util;
20
21 import java.io.BufferedReader;
22 import java.io.ByteArrayInputStream;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.InputStreamReader;
28 import java.nio.charset.StandardCharsets;
29 import java.security.MessageDigest;
30 import java.security.NoSuchAlgorithmException;
31 import java.util.Collection;
32 import java.util.LinkedHashMap;
33 import java.util.Map;
34
35 /**
36 * A utility class to assist in the verification and generation of checksums.
37 *
38 * @deprecated the use of class should be avoided, see {@link StringDigestUtil} and file processor in SPI module
39 */
40 @Deprecated
41 public final class ChecksumUtils {
42 /**
43 * Upper bound (in characters) for data read from a checksum file, see {@link #read(File)}. Every sane
44 * checksum file format fits well within this limit; longer input is rejected as malformed instead of being
45 * buffered into memory.
46 */
47 private static final int MAX_CHECKSUM_FILE_CHARS = 8192;
48
49 private ChecksumUtils() {
50 // hide constructor
51 }
52
53 /**
54 * Extracts the checksum from the specified file.
55 *
56 * @param checksumFile the path to the checksum file, must not be {@code null}
57 * @return the checksum stored in the file, never {@code null}
58 * @throws IOException if the checksum does not exist or could not be read for other reasons
59 * @deprecated use SPI FileProcessor to read and write checksum files
60 */
61 @Deprecated
62 public static String read(File checksumFile) throws IOException {
63 String checksum;
64 try (BufferedReader br = new BufferedReader(
65 new InputStreamReader(new FileInputStream(checksumFile), StandardCharsets.UTF_8), 512)) {
66 checksum = readFirstNonEmptyLine(br, checksumFile.toString());
67 }
68
69 if (isAlgorithmHeaderFormat(checksum)) {
70 int lastSpacePos = checksum.lastIndexOf(' ');
71 checksum = checksum.substring(lastSpacePos + 1);
72 } else {
73 int spacePos = checksum.indexOf(' ');
74
75 if (spacePos != -1) {
76 checksum = checksum.substring(0, spacePos);
77 }
78 }
79
80 return checksum;
81 }
82
83 /**
84 * Reads the first non-empty line, enforcing {@link #MAX_CHECKSUM_FILE_CHARS} on the total amount of data
85 * consumed. Returns the trimmed line, or an empty string if the stream holds no non-empty line.
86 */
87 private static String readFirstNonEmptyLine(BufferedReader reader, String source) throws IOException {
88 StringBuilder buffer = new StringBuilder(64);
89 int read = 0;
90 int c;
91 while ((c = reader.read()) != -1) {
92 if (++read > MAX_CHECKSUM_FILE_CHARS) {
93 throw new IOException("Checksum file " + source + " is malformed: longer than "
94 + MAX_CHECKSUM_FILE_CHARS + " characters");
95 }
96 if (c == '\n' || c == '\r') {
97 String line = buffer.toString().trim();
98 if (!line.isEmpty()) {
99 return line;
100 }
101 buffer.setLength(0);
102 } else {
103 buffer.append((char) c);
104 }
105 }
106 return buffer.toString().trim();
107 }
108
109 /**
110 * Non-backtracking equivalent of {@code line.matches(".+= [0-9A-Fa-f]+")}: at least one character, followed
111 * by "= ", followed by one or more hex digits reaching the end of the line ("<algorithm> (<file>)
112 * = <hex>" style checksum lines).
113 */
114 private static boolean isAlgorithmHeaderFormat(String line) {
115 int lastSpacePos = line.lastIndexOf(' ');
116 if (lastSpacePos < 2 || lastSpacePos == line.length() - 1 || line.charAt(lastSpacePos - 1) != '=') {
117 return false;
118 }
119 for (int i = lastSpacePos + 1; i < line.length(); i++) {
120 char ch = line.charAt(i);
121 boolean hex = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F');
122 if (!hex) {
123 return false;
124 }
125 }
126 return true;
127 }
128
129 /**
130 * Calculates checksums for the specified file.
131 *
132 * @param dataFile the file for which to calculate checksums, must not be {@code null}
133 * @param algos the names of checksum algorithms (cf. {@link MessageDigest#getInstance(String)} to use, must not be
134 * {@code null}.
135 * @return the calculated checksums, indexed by algorithm name, or the exception that occurred while trying to
136 * calculate it, never {@code null}
137 * @throws IOException if the data file could not be read
138 * @deprecated use SPI checksum selector instead
139 */
140 @Deprecated
141 public static Map<String, Object> calc(File dataFile, Collection<String> algos) throws IOException {
142 return calc(new FileInputStream(dataFile), algos);
143 }
144
145 /**
146 * @deprecated use SPI checksum selector instead
147 */
148 @Deprecated
149 public static Map<String, Object> calc(byte[] dataBytes, Collection<String> algos) throws IOException {
150 return calc(new ByteArrayInputStream(dataBytes), algos);
151 }
152
153 private static Map<String, Object> calc(InputStream data, Collection<String> algos) throws IOException {
154 Map<String, Object> results = new LinkedHashMap<>();
155
156 Map<String, MessageDigest> digests = new LinkedHashMap<>();
157 for (String algo : algos) {
158 try {
159 digests.put(algo, MessageDigest.getInstance(algo));
160 } catch (NoSuchAlgorithmException e) {
161 results.put(algo, e);
162 }
163 }
164
165 try (InputStream in = data) {
166 for (byte[] buffer = new byte[32 * 1024]; ; ) {
167 int read = in.read(buffer);
168 if (read < 0) {
169 break;
170 }
171 for (MessageDigest digest : digests.values()) {
172 digest.update(buffer, 0, read);
173 }
174 }
175 }
176
177 for (Map.Entry<String, MessageDigest> entry : digests.entrySet()) {
178 byte[] bytes = entry.getValue().digest();
179
180 results.put(entry.getKey(), toHexString(bytes));
181 }
182
183 return results;
184 }
185
186 /**
187 * Creates a hexadecimal representation of the specified bytes. Each byte is converted into a two-digit hex number
188 * and appended to the result with no separator between consecutive bytes.
189 *
190 * @param bytes the bytes to represent in hex notation, may be be {@code null}
191 * @return the hexadecimal representation of the input or {@code null} if the input was {@code null}
192 */
193 public static String toHexString(byte[] bytes) {
194 return StringDigestUtil.toHexString(bytes);
195 }
196
197 /**
198 * Creates a byte array out of hexadecimal representation of the specified bytes. If input string is {@code null},
199 * {@code null} is returned. Input value must have even length (due hex encoding = 2 chars one byte).
200 *
201 * @param hexString the hexString to convert to byte array, may be {@code null}
202 * @return the byte array of the input or {@code null} if the input was {@code null}
203 * @since 1.8.0
204 */
205 public static byte[] fromHexString(String hexString) {
206 return StringDigestUtil.fromHexString(hexString);
207 }
208 }