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 */
038public final class ChecksumUtils {
039
040    private ChecksumUtils() {
041        // hide constructor
042    }
043
044    /**
045     * Extracts the checksum from the specified file.
046     *
047     * @param checksumFile The path to the checksum file, must not be {@code null}.
048     * @return The checksum stored in the file, never {@code null}.
049     * @throws IOException If the checksum does not exist or could not be read for other reasons.
050     * @deprecated Use SPI FileProcessor to read and write checksum files.
051     */
052    @Deprecated
053    public static String read(File checksumFile) throws IOException {
054        String checksum = "";
055        try (BufferedReader br = new BufferedReader(
056                new InputStreamReader(new FileInputStream(checksumFile), StandardCharsets.UTF_8), 512)) {
057            while (true) {
058                String line = br.readLine();
059                if (line == null) {
060                    break;
061                }
062                line = line.trim();
063                if (line.length() > 0) {
064                    checksum = line;
065                    break;
066                }
067            }
068        }
069
070        if (checksum.matches(".+= [0-9A-Fa-f]+")) {
071            int lastSpacePos = checksum.lastIndexOf(' ');
072            checksum = checksum.substring(lastSpacePos + 1);
073        } else {
074            int spacePos = checksum.indexOf(' ');
075
076            if (spacePos != -1) {
077                checksum = checksum.substring(0, spacePos);
078            }
079        }
080
081        return checksum;
082    }
083
084    /**
085     * Calculates checksums for the specified file.
086     *
087     * @param dataFile The file for which to calculate checksums, must not be {@code null}.
088     * @param algos The names of checksum algorithms (cf. {@link MessageDigest#getInstance(String)} to use, must not be
089     *            {@code null}.
090     * @return The calculated checksums, indexed by algorithm name, or the exception that occurred while trying to
091     *         calculate it, never {@code null}.
092     * @throws IOException If the data file could not be read.
093     * @deprecated Use SPI checksum selector instead.
094     */
095    @Deprecated
096    public static Map<String, Object> calc(File dataFile, Collection<String> algos) throws IOException {
097        return calc(new FileInputStream(dataFile), algos);
098    }
099
100    /**
101     * @deprecated Use SPI checksum selector instead.
102     */
103    @Deprecated
104    public static Map<String, Object> calc(byte[] dataBytes, Collection<String> algos) throws IOException {
105        return calc(new ByteArrayInputStream(dataBytes), algos);
106    }
107
108    private static Map<String, Object> calc(InputStream data, Collection<String> algos) throws IOException {
109        Map<String, Object> results = new LinkedHashMap<>();
110
111        Map<String, MessageDigest> digests = new LinkedHashMap<>();
112        for (String algo : algos) {
113            try {
114                digests.put(algo, MessageDigest.getInstance(algo));
115            } catch (NoSuchAlgorithmException e) {
116                results.put(algo, e);
117            }
118        }
119
120        try (InputStream in = data) {
121            for (byte[] buffer = new byte[32 * 1024]; ; ) {
122                int read = in.read(buffer);
123                if (read < 0) {
124                    break;
125                }
126                for (MessageDigest digest : digests.values()) {
127                    digest.update(buffer, 0, read);
128                }
129            }
130        }
131
132        for (Map.Entry<String, MessageDigest> entry : digests.entrySet()) {
133            byte[] bytes = entry.getValue().digest();
134
135            results.put(entry.getKey(), toHexString(bytes));
136        }
137
138        return results;
139    }
140
141    /**
142     * Creates a hexadecimal representation of the specified bytes. Each byte is converted into a two-digit hex number
143     * and appended to the result with no separator between consecutive bytes.
144     *
145     * @param bytes The bytes to represent in hex notation, may be be {@code null}.
146     * @return The hexadecimal representation of the input or {@code null} if the input was {@code null}.
147     */
148    @SuppressWarnings("checkstyle:magicnumber")
149    public static String toHexString(byte[] bytes) {
150        if (bytes == null) {
151            return null;
152        }
153
154        StringBuilder buffer = new StringBuilder(bytes.length * 2);
155
156        for (byte aByte : bytes) {
157            int b = aByte & 0xFF;
158            if (b < 0x10) {
159                buffer.append('0');
160            }
161            buffer.append(Integer.toHexString(b));
162        }
163
164        return buffer.toString();
165    }
166
167    /**
168     * Creates a byte array out of hexadecimal representation of the specified bytes. If input string is {@code null},
169     * {@code null} is returned. Input value must have even length (due hex encoding = 2 chars one byte).
170     *
171     * @param hexString The hexString to convert to byte array, may be {@code null}.
172     * @return The byte array of the input or {@code null} if the input was {@code null}.
173     * @since 1.8.0
174     */
175    @SuppressWarnings("checkstyle:magicnumber")
176    public static byte[] fromHexString(String hexString) {
177        if (hexString == null) {
178            return null;
179        }
180        if (hexString.isEmpty()) {
181            return new byte[] {};
182        }
183        int len = hexString.length();
184        if (len % 2 != 0) {
185            throw new IllegalArgumentException("hexString length not even");
186        }
187        byte[] data = new byte[len / 2];
188        for (int i = 0; i < len; i += 2) {
189            data[i / 2] = (byte)
190                    ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
191        }
192        return data;
193    }
194}