content: 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;
      }
  }
  