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.internal.impl;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.io.BufferedReader;
026import java.io.IOException;
027import java.nio.charset.StandardCharsets;
028import java.nio.file.Files;
029import java.nio.file.Path;
030
031import org.eclipse.aether.spi.io.ChecksumProcessor;
032import org.eclipse.aether.spi.io.PathProcessor;
033
034import static java.util.Objects.requireNonNull;
035
036/**
037 * A utility class helping with file-based operations.
038 */
039@Singleton
040@Named
041public class DefaultChecksumProcessor implements ChecksumProcessor {
042    /**
043     * Upper bound (in characters) for data read from a checksum file. Every sane checksum file format fits well
044     * within this limit (the longest is "SHA-512 (<file name>) = <128 hex chars>"). Checksum files
045     * are fetched from remote repositories: without a bound, a hostile repository answering a checksum request
046     * with a multi-gigabyte single-line body would be buffered wholesale into memory, exhausting the build JVM
047     * heap. Longer input is rejected as malformed instead of being buffered.
048     */
049    static final int MAX_CHECKSUM_FILE_CHARS = 8192;
050
051    private final PathProcessor pathProcessor;
052
053    @Inject
054    public DefaultChecksumProcessor(PathProcessor pathProcessor) {
055        this.pathProcessor = requireNonNull(pathProcessor);
056    }
057
058    @Override
059    public String readChecksum(final Path checksumPath) throws IOException {
060        String checksum;
061        try (BufferedReader br = Files.newBufferedReader(checksumPath, StandardCharsets.UTF_8)) {
062            checksum = readFirstNonEmptyLine(br, checksumPath.toString());
063        }
064
065        if (isAlgorithmHeaderFormat(checksum)) {
066            int lastSpacePos = checksum.lastIndexOf(' ');
067            checksum = checksum.substring(lastSpacePos + 1);
068        } else {
069            int spacePos = checksum.indexOf(' ');
070
071            if (spacePos != -1) {
072                checksum = checksum.substring(0, spacePos);
073            }
074        }
075
076        return checksum;
077    }
078
079    /**
080     * Reads the first non-empty line, enforcing {@link #MAX_CHECKSUM_FILE_CHARS} on the total amount of data
081     * consumed. Returns the trimmed line, or an empty string if the stream holds no non-empty line.
082     */
083    static String readFirstNonEmptyLine(BufferedReader reader, String source) throws IOException {
084        StringBuilder buffer = new StringBuilder(64);
085        int read = 0;
086        int c;
087        while ((c = reader.read()) != -1) {
088            if (++read > MAX_CHECKSUM_FILE_CHARS) {
089                throw new IOException("Checksum file " + source + " is malformed: longer than "
090                        + MAX_CHECKSUM_FILE_CHARS + " characters");
091            }
092            if (c == '\n' || c == '\r') {
093                String line = buffer.toString().trim();
094                if (!line.isEmpty()) {
095                    return line;
096                }
097                buffer.setLength(0);
098            } else {
099                buffer.append((char) c);
100            }
101        }
102        return buffer.toString().trim();
103    }
104
105    /**
106     * Non-backtracking equivalent of {@code line.matches(".+= [0-9A-Fa-f]+")}: at least one character, followed
107     * by "= ", followed by one or more hex digits reaching the end of the line ("<algorithm> (<file>)
108     * = <hex>" style checksum lines).
109     */
110    static boolean isAlgorithmHeaderFormat(String line) {
111        int lastSpacePos = line.lastIndexOf(' ');
112        if (lastSpacePos < 2 || lastSpacePos == line.length() - 1 || line.charAt(lastSpacePos - 1) != '=') {
113            return false;
114        }
115        for (int i = lastSpacePos + 1; i < line.length(); i++) {
116            char ch = line.charAt(i);
117            boolean hex = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F');
118            if (!hex) {
119                return false;
120            }
121        }
122        return true;
123    }
124
125    @Override
126    public void writeChecksum(Path target, String checksum) throws IOException {
127        // for now do exactly same as happened before, but FileProcessor is a component and can be replaced
128        pathProcessor.write(target, checksum);
129    }
130}