| package org.apache.archiva.checksum; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import org.apache.archiva.common.utils.FileUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.StandardOpenOption; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import static org.apache.archiva.checksum.ChecksumValidationException.ValidationError.BAD_CHECKSUM_FILE; |
| import static org.apache.archiva.checksum.ChecksumValidationException.ValidationError.BAD_CHECKSUM_FILE_REF; |
| |
| /** |
| * ChecksummedFile |
| * <p>Terminology:</p> |
| * <dl> |
| * <dt>Checksum File</dt> |
| * <dd>The file that contains the previously calculated checksum value for the reference file. |
| * This is a text file with the extension ".sha1" or ".md5", and contains a single entry |
| * consisting of an optional reference filename, and a checksum string. |
| * </dd> |
| * <dt>Reference File</dt> |
| * <dd>The file that is being referenced in the checksum file.</dd> |
| * </dl> |
| */ |
| public class ChecksummedFile |
| { |
| |
| private static Charset FILE_ENCODING = Charset.forName( "UTF-8" ); |
| |
| private final Logger log = LoggerFactory.getLogger( ChecksummedFile.class ); |
| |
| private static final Pattern METADATA_PATTERN = Pattern.compile( "maven-metadata-\\S*.xml" ); |
| |
| private final Path referenceFile; |
| |
| /** |
| * Construct a ChecksummedFile object. |
| * |
| * @param referenceFile |
| */ |
| public ChecksummedFile( final Path referenceFile ) |
| { |
| this.referenceFile = referenceFile; |
| } |
| |
| |
| public static ChecksumReference getFromChecksumFile( Path checksumFile ) |
| { |
| ChecksumAlgorithm alg = ChecksumAlgorithm.getByExtension( checksumFile ); |
| ChecksummedFile file = new ChecksummedFile( getReferenceFile( checksumFile ) ); |
| return new ChecksumReference( file, alg, checksumFile ); |
| } |
| |
| private static Path getReferenceFile( Path checksumFile ) |
| { |
| String fileName = checksumFile.getFileName( ).toString( ); |
| return checksumFile.resolveSibling( fileName.substring( 0, fileName.lastIndexOf( '.' ) ) ); |
| } |
| |
| /** |
| * Calculate the checksum based on a given checksum. |
| * |
| * @param checksumAlgorithm the algorithm to use. |
| * @return the checksum string for the file. |
| * @throws IOException if unable to calculate the checksum. |
| */ |
| public String calculateChecksum( ChecksumAlgorithm checksumAlgorithm ) |
| throws IOException |
| { |
| |
| Checksum checksum = new Checksum( checksumAlgorithm ); |
| ChecksumUtil.update(checksum, referenceFile ); |
| return checksum.getChecksum( ); |
| } |
| |
| /** |
| * Writes a checksum file for the referenceFile. |
| * |
| * @param checksumAlgorithm the hash to use. |
| * @return the checksum File that was created. |
| * @throws IOException if there was a problem either reading the referenceFile, or writing the checksum file. |
| */ |
| public Path writeFile(ChecksumAlgorithm checksumAlgorithm ) |
| throws IOException |
| { |
| Path checksumFile = referenceFile.resolveSibling( referenceFile.getFileName( ) + "." + checksumAlgorithm.getDefaultExtension() ); |
| Files.deleteIfExists( checksumFile ); |
| String checksum = calculateChecksum( checksumAlgorithm ); |
| Files.write( checksumFile, // |
| ( checksum + " " + referenceFile.getFileName( ).toString( ) ).getBytes( ), // |
| StandardOpenOption.CREATE_NEW ); |
| return checksumFile; |
| } |
| |
| /** |
| * Get the checksum file for the reference file and hash. |
| * It returns a file for the given checksum, if one exists with one of the possible extensions. |
| * If it does not exist, a default path will be returned. |
| * |
| * @param checksumAlgorithm the hash that we are interested in. |
| * @return the checksum file to return |
| */ |
| public Path getChecksumFile( ChecksumAlgorithm checksumAlgorithm ) |
| { |
| for ( String ext : checksumAlgorithm.getExt( ) ) |
| { |
| Path file = referenceFile.resolveSibling( referenceFile.getFileName( ) + "." + ext ); |
| if ( Files.exists( file ) ) |
| { |
| return file; |
| } |
| } |
| return referenceFile.resolveSibling( referenceFile.getFileName( ) + "." + checksumAlgorithm.getDefaultExtension() ); |
| } |
| |
| /** |
| * <p> |
| * Given a checksum file, check to see if the file it represents is valid according to the checksum. |
| * </p> |
| * <p> |
| * NOTE: Only supports single file checksums of type MD5 or SHA1. |
| * </p> |
| * |
| * @param algorithm the algorithms to check for. |
| * @return true if the checksum is valid for the file it represents. or if the checksum file does not exist. |
| * @throws IOException if the reading of the checksumFile or the file it refers to fails. |
| */ |
| public boolean isValidChecksum( ChecksumAlgorithm algorithm) throws ChecksumValidationException |
| { |
| return isValidChecksum( algorithm, false ); |
| } |
| public boolean isValidChecksum( ChecksumAlgorithm algorithm, boolean throwExceptions ) |
| throws ChecksumValidationException |
| { |
| return isValidChecksums( Arrays.asList( algorithm ), throwExceptions ); |
| } |
| |
| /** |
| * Of any checksum files present, validate that the reference file conforms |
| * the to the checksum. |
| * |
| * @param algorithms the algorithms to check for. |
| * @return true if the checksums report that the the reference file is valid, false if invalid. |
| */ |
| public boolean isValidChecksums( List<ChecksumAlgorithm> algorithms) throws ChecksumValidationException |
| { |
| return isValidChecksums( algorithms, false ); |
| } |
| |
| /** |
| * Checks if the checksum files are valid for the referenced file. |
| * It tries to find a checksum file for each algorithm in the same directory as the referenceFile. |
| * The method returns true, if at least one checksum file exists for one of the given algorithms |
| * and all existing checksum files are valid. |
| * |
| * This method throws only exceptions, if throwExceptions is true. Otherwise false will be returned instead. |
| * |
| * It verifies only the existing checksum files. If the checksum file for a particular algorithm does not exist, |
| * but others exist and are valid, it will return true. |
| * |
| * @param algorithms The algorithms to verify |
| * @param throwExceptions If true, exceptions will be thrown, otherwise false will be returned, if a exception occurred. |
| * @return True, if it is valid for all existing checksum files, otherwise false. |
| * @throws ChecksumValidationException |
| */ |
| public boolean isValidChecksums( List<ChecksumAlgorithm> algorithms, boolean throwExceptions) throws ChecksumValidationException |
| { |
| |
| List<Checksum> checksums; |
| // Parse file once, for all checksums. |
| try |
| { |
| checksums = ChecksumUtil.initializeChecksums( referenceFile, algorithms ); |
| } |
| catch (IOException e ) |
| { |
| log.warn( "Unable to update checksum:{}", e.getMessage( ) ); |
| if (throwExceptions) { |
| if (e instanceof FileNotFoundException) { |
| throw new ChecksumValidationException(ChecksumValidationException.ValidationError.FILE_NOT_FOUND, e); |
| } else { |
| throw new ChecksumValidationException(ChecksumValidationException.ValidationError.READ_ERROR, e); |
| } |
| } else { |
| return false; |
| } |
| } |
| |
| boolean valid = true; |
| boolean fileExists = false; |
| |
| // No file exists -> return false |
| // if at least one file exists: |
| // -> all existing files must be valid |
| |
| // check the checksum files |
| try |
| { |
| |
| for ( Checksum checksum : checksums ) |
| { |
| ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm( ); |
| Path checksumFile = getChecksumFile( checksumAlgorithm ); |
| |
| if (Files.exists(checksumFile)) { |
| fileExists = true; |
| String expectedChecksum = parseChecksum(checksumFile, checksumAlgorithm, referenceFile.getFileName().toString(), FILE_ENCODING); |
| |
| valid &= checksum.compare(expectedChecksum); |
| } |
| } |
| } |
| catch ( ChecksumValidationException e ) |
| { |
| log.warn( "Unable to read / parse checksum: {}", e.getMessage( ) ); |
| if (throwExceptions) { |
| throw e; |
| } else |
| { |
| return false; |
| } |
| } |
| |
| return fileExists && valid; |
| } |
| |
| public Path getReferenceFile( ) |
| { |
| return referenceFile; |
| } |
| |
| |
| |
| public UpdateStatusList fixChecksum(ChecksumAlgorithm algorithm) { |
| return fixChecksums( Arrays.asList(algorithm) ); |
| } |
| |
| /** |
| * Writes a checksum file, if it does not exist or if it exists and has a different |
| * checksum value. |
| * |
| * @param algorithms the hashes to check for. |
| * @return true if checksums were created successfully. |
| */ |
| public UpdateStatusList fixChecksums( List<ChecksumAlgorithm> algorithms ) |
| { |
| UpdateStatusList result = UpdateStatusList.INITIALIZE(algorithms); |
| List<Checksum> checksums; |
| |
| |
| try |
| { |
| // Parse file once, for all checksums. |
| checksums = ChecksumUtil.initializeChecksums(getReferenceFile(), algorithms); |
| } |
| catch (IOException e ) |
| { |
| log.warn( e.getMessage( ), e ); |
| result.setTotalError(e); |
| return result; |
| } |
| // Any checksums? |
| if ( checksums.isEmpty( ) ) |
| { |
| // No checksum objects, no checksum files, default to is valid. |
| return result; |
| } |
| |
| boolean valid = true; |
| |
| // check the hash files |
| for ( Checksum checksum : checksums ) |
| { |
| ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm( ); |
| try |
| { |
| Path checksumFile = getChecksumFile( checksumAlgorithm ); |
| if ( Files.exists( checksumFile ) ) |
| { |
| String expectedChecksum; |
| try |
| { |
| expectedChecksum = parseChecksum( checksumFile, checksumAlgorithm, referenceFile.getFileName( ).toString( ), FILE_ENCODING ); |
| } catch (ChecksumValidationException ex) { |
| expectedChecksum = ""; |
| } |
| |
| if ( !checksum.compare( expectedChecksum ) ) |
| { |
| // overwrite checksum file |
| writeChecksumFile( checksumFile, FILE_ENCODING, checksum.getChecksum( ) ); |
| result.setStatus(checksumAlgorithm,UpdateStatus.UPDATED); |
| } |
| } |
| else |
| { |
| writeChecksumFile( checksumFile, FILE_ENCODING, checksum.getChecksum( ) ); |
| result.setStatus(checksumAlgorithm, UpdateStatus.CREATED); |
| } |
| } |
| catch ( ChecksumValidationException e ) |
| { |
| log.warn( e.getMessage( ), e ); |
| result.setErrorStatus(checksumAlgorithm, e); |
| } |
| } |
| |
| return result; |
| |
| } |
| |
| private void writeChecksumFile( Path checksumFile, Charset encoding, String checksumHex ) |
| { |
| FileUtils.writeStringToFile( checksumFile, encoding, checksumHex + " " + referenceFile.getFileName( ).toString( ) ); |
| } |
| |
| private boolean isValidChecksumPattern( String filename, String path ) |
| { |
| // check if it is a remote metadata file |
| |
| Matcher m = METADATA_PATTERN.matcher( path ); |
| if ( m.matches( ) ) |
| { |
| return filename.endsWith( path ) || ( "-".equals( filename ) ) || filename.endsWith( "maven-metadata.xml" ); |
| } |
| |
| return filename.endsWith( path ) || ( "-".equals( filename ) ); |
| } |
| |
| /** |
| * Parse a checksum string. |
| * <p> |
| * Validate the expected path, and expected checksum algorithm, then return |
| * the trimmed checksum hex string. |
| * </p> |
| * |
| * @param checksumFile The file where the checksum is stored |
| * @param checksumAlgorithm The checksum algorithm to check |
| * @param fileName The filename of the reference file |
| * @return |
| * @throws IOException |
| */ |
| public String parseChecksum( Path checksumFile, ChecksumAlgorithm checksumAlgorithm, String fileName, Charset encoding ) |
| throws ChecksumValidationException |
| { |
| ChecksumFileContent fc = parseChecksumFile( checksumFile, checksumAlgorithm, encoding ); |
| if ( fc.isFormatMatch() && !isValidChecksumPattern( fc.getFileReference( ), fileName ) ) |
| { |
| throw new ChecksumValidationException(BAD_CHECKSUM_FILE_REF, |
| "The file reference '" + fc.getFileReference( ) + "' in the checksum file does not match expected file: '" + fileName + "'" ); |
| } else if (!fc.isFormatMatch()) { |
| throw new ChecksumValidationException( BAD_CHECKSUM_FILE, "The checksum file content could not be parsed: "+checksumFile ); |
| } |
| return fc.getChecksum( ); |
| |
| } |
| public ChecksumFileContent parseChecksumFile( Path checksumFile, ChecksumAlgorithm checksumAlgorithm, Charset encoding ) |
| { |
| ChecksumFileContent fc = new ChecksumFileContent( ); |
| String rawChecksumString = FileUtils.readFileToString( checksumFile, encoding ); |
| String trimmedChecksum = rawChecksumString.replace( '\n', ' ' ).trim( ); |
| |
| // Free-BSD / openssl |
| String regex = checksumAlgorithm.getType( ) + "\\s*\\(([^)]*)\\)\\s*=\\s*([a-fA-F0-9]+)"; |
| Matcher m = Pattern.compile( regex ).matcher( trimmedChecksum ); |
| if ( m.matches( ) ) |
| { |
| fc.setFileReference( m.group( 1 ) ); |
| fc.setChecksum( m.group( 2 ) ); |
| fc.setFormatMatch( true ); |
| } |
| else |
| { |
| // GNU tools |
| m = Pattern.compile( "([a-fA-F0-9]+)\\s+\\*?(.+)" ).matcher( trimmedChecksum ); |
| if ( m.matches( ) ) |
| { |
| fc.setFileReference( m.group( 2 ) ); |
| fc.setChecksum( m.group( 1 ) ); |
| fc.setFormatMatch( true ); |
| } |
| else |
| { |
| fc.setFileReference( "" ); |
| fc.setChecksum( trimmedChecksum ); |
| fc.setFormatMatch( false ); |
| } |
| } |
| return fc; |
| } |
| } |