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}