001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.util;
020
021import java.io.BufferedReader;
022import java.io.ByteArrayInputStream;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.nio.charset.StandardCharsets;
029import java.security.MessageDigest;
030import java.security.NoSuchAlgorithmException;
031import java.util.Collection;
032import java.util.LinkedHashMap;
033import java.util.Map;
034
035/**
036 * A utility class to assist in the verification and generation of checksums.
037 *
038 * @deprecated the use of class should be avoided, see {@link StringDigestUtil} and file processor in SPI module
039 */
040@Deprecated
041public final class ChecksumUtils {
042    /**
043     * Upper bound (in characters) for data read from a checksum file, see {@link #read(File)}. Every sane
044     * checksum file format fits well within this limit; longer input is rejected as malformed instead of being
045     * buffered into memory.
046     */
047    private static final int MAX_CHECKSUM_FILE_CHARS = 8192;
048
049    private ChecksumUtils() {
050        // hide constructor
051    }
052
053    /**
054     * Extracts the checksum from the specified file.
055     *
056     * @param checksumFile the path to the checksum file, must not be {@code null}
057     * @return the checksum stored in the file, never {@code null}
058     * @throws IOException if the checksum does not exist or could not be read for other reasons
059     * @deprecated use SPI FileProcessor to read and write checksum files
060     */
061    @Deprecated
062    public static String read(File checksumFile) throws IOException {
063        String checksum;
064        try (BufferedReader br = new BufferedReader(
065                new InputStreamReader(new FileInputStream(checksumFile), StandardCharsets.UTF_8), 512)) {
066            checksum = readFirstNonEmptyLine(br, checksumFile.toString());
067        }
068
069        if (isAlgorithmHeaderFormat(checksum)) {
070            int lastSpacePos = checksum.lastIndexOf(' ');
071            checksum = checksum.substring(lastSpacePos + 1);
072        } else {
073            int spacePos = checksum.indexOf(' ');
074
075            if (spacePos != -1) {
076                checksum = checksum.substring(0, spacePos);
077            }
078        }
079
080        return checksum;
081    }
082
083    /**
084     * Reads the first non-empty line, enforcing {@link #MAX_CHECKSUM_FILE_CHARS} on the total amount of data
085     * consumed. Returns the trimmed line, or an empty string if the stream holds no non-empty line.
086     */
087    private static String readFirstNonEmptyLine(BufferedReader reader, String source) throws IOException {
088        StringBuilder buffer = new StringBuilder(64);
089        int read = 0;
090        int c;
091        while ((c = reader.read()) != -1) {
092            if (++read > MAX_CHECKSUM_FILE_CHARS) {
093                throw new IOException("Checksum file " + source + " is malformed: longer than "
094                        + MAX_CHECKSUM_FILE_CHARS + " characters");
095            }
096            if (c == '\n' || c == '\r') {
097                String line = buffer.toString().trim();
098                if (!line.isEmpty()) {
099                    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}