This project has retired. For details please refer to its Attic page.
Source code
001package org.apache.archiva.checksum;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.archiva.common.utils.FileUtils;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026import java.io.FileNotFoundException;
027import java.io.IOException;
028import java.nio.charset.Charset;
029import java.nio.file.Files;
030import java.nio.file.Path;
031import java.nio.file.StandardOpenOption;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.List;
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037
038import static org.apache.archiva.checksum.ChecksumValidationException.ValidationError.BAD_CHECKSUM_FILE;
039import static org.apache.archiva.checksum.ChecksumValidationException.ValidationError.BAD_CHECKSUM_FILE_REF;
040
041/**
042 * ChecksummedFile
043 * <p>Terminology:</p>
044 * <dl>
045 * <dt>Checksum File</dt>
046 * <dd>The file that contains the previously calculated checksum value for the reference file.
047 * This is a text file with the extension ".sha1" or ".md5", and contains a single entry
048 * consisting of an optional reference filename, and a checksum string.
049 * </dd>
050 * <dt>Reference File</dt>
051 * <dd>The file that is being referenced in the checksum file.</dd>
052 * </dl>
053 */
054public class ChecksummedFile
055{
056
057    private static Charset FILE_ENCODING = Charset.forName( "UTF-8" );
058
059    private final Logger log = LoggerFactory.getLogger( ChecksummedFile.class );
060
061    private static final Pattern METADATA_PATTERN = Pattern.compile( "maven-metadata-\\S*.xml" );
062
063    private final Path referenceFile;
064
065    /**
066     * Construct a ChecksummedFile object.
067     *
068     * @param referenceFile
069     */
070    public ChecksummedFile( final Path referenceFile )
071    {
072        this.referenceFile = referenceFile;
073    }
074
075
076    public static ChecksumReference getFromChecksumFile( Path checksumFile )
077    {
078        ChecksumAlgorithm alg = ChecksumAlgorithm.getByExtension( checksumFile );
079        ChecksummedFile file = new ChecksummedFile( getReferenceFile( checksumFile ) );
080        return new ChecksumReference( file, alg, checksumFile );
081    }
082
083    private static Path getReferenceFile( Path checksumFile )
084    {
085        String fileName = checksumFile.getFileName( ).toString( );
086        return checksumFile.resolveSibling( fileName.substring( 0, fileName.lastIndexOf( '.' ) ) );
087    }
088
089    /**
090     * Calculate the checksum based on a given checksum.
091     *
092     * @param checksumAlgorithm the algorithm to use.
093     * @return the checksum string for the file.
094     * @throws IOException if unable to calculate the checksum.
095     */
096    public String calculateChecksum( ChecksumAlgorithm checksumAlgorithm )
097        throws IOException
098    {
099
100        Checksum checksum = new Checksum( checksumAlgorithm );
101        ChecksumUtil.update(checksum, referenceFile );
102        return checksum.getChecksum( );
103    }
104
105    /**
106     * Writes a checksum file for the referenceFile.
107     *
108     * @param checksumAlgorithm the hash to use.
109     * @return the checksum File that was created.
110     * @throws IOException if there was a problem either reading the referenceFile, or writing the checksum file.
111     */
112    public Path writeFile(ChecksumAlgorithm checksumAlgorithm )
113        throws IOException
114    {
115        Path checksumFile = referenceFile.resolveSibling( referenceFile.getFileName( ) + "." + checksumAlgorithm.getDefaultExtension() );
116        Files.deleteIfExists( checksumFile );
117        String checksum = calculateChecksum( checksumAlgorithm );
118        Files.write( checksumFile, //
119            ( checksum + "  " + referenceFile.getFileName( ).toString( ) ).getBytes( ), //
120            StandardOpenOption.CREATE_NEW );
121        return checksumFile;
122    }
123
124    /**
125     * Get the checksum file for the reference file and hash.
126     * It returns a file for the given checksum, if one exists with one of the possible extensions.
127     * If it does not exist, a default path will be returned.
128     *
129     * @param checksumAlgorithm the hash that we are interested in.
130     * @return the checksum file to return
131     */
132    public Path getChecksumFile( ChecksumAlgorithm checksumAlgorithm )
133    {
134        for ( String ext : checksumAlgorithm.getExt( ) )
135        {
136            Path file = referenceFile.resolveSibling( referenceFile.getFileName( ) + "." + checksumAlgorithm.getExt( ) );
137            if ( Files.exists( file ) )
138            {
139                return file;
140            }
141        }
142        return referenceFile.resolveSibling( referenceFile.getFileName( ) + "." + checksumAlgorithm.getDefaultExtension() );
143    }
144
145    /**
146     * <p>
147     * Given a checksum file, check to see if the file it represents is valid according to the checksum.
148     * </p>
149     * <p>
150     * NOTE: Only supports single file checksums of type MD5 or SHA1.
151     * </p>
152     *
153     * @param algorithm the algorithms to check for.
154     * @return true if the checksum is valid for the file it represents. or if the checksum file does not exist.
155     * @throws IOException if the reading of the checksumFile or the file it refers to fails.
156     */
157    public boolean isValidChecksum( ChecksumAlgorithm algorithm) throws ChecksumValidationException
158    {
159        return isValidChecksum( algorithm, false );
160    }
161    public boolean isValidChecksum( ChecksumAlgorithm algorithm, boolean throwExceptions )
162        throws ChecksumValidationException
163    {
164        return isValidChecksums( Arrays.asList( algorithm ), throwExceptions );
165    }
166
167    /**
168     * Of any checksum files present, validate that the reference file conforms
169     * the to the checksum.
170     *
171     * @param algorithms the algorithms to check for.
172     * @return true if the checksums report that the the reference file is valid, false if invalid.
173     */
174    public boolean isValidChecksums( List<ChecksumAlgorithm> algorithms) throws ChecksumValidationException
175    {
176        return isValidChecksums( algorithms, false );
177    }
178
179    /**
180     * Checks if the checksum files are valid for the referenced file.
181     * It tries to find a checksum file for each algorithm in the same directory as the referenceFile.
182     * The method returns true, if at least one checksum file exists for one of the given algorithms
183     * and all existing checksum files are valid.
184     *
185     * This method throws only exceptions, if throwExceptions is true. Otherwise false will be returned instead.
186     *
187     * It verifies only the existing checksum files. If the checksum file for a particular algorithm does not exist,
188     * but others exist and are valid, it will return true.
189     *
190     * @param algorithms The algorithms to verify
191     * @param throwExceptions If true, exceptions will be thrown, otherwise false will be returned, if a exception occurred.
192     * @return True, if it is valid for all existing checksum files, otherwise false.
193     * @throws ChecksumValidationException
194     */
195    public boolean isValidChecksums( List<ChecksumAlgorithm> algorithms, boolean throwExceptions) throws ChecksumValidationException
196    {
197
198        List<Checksum> checksums;
199        // Parse file once, for all checksums.
200        try
201        {
202            checksums = ChecksumUtil.initializeChecksums( referenceFile, algorithms );
203        }
204        catch (IOException e )
205        {
206            log.warn( "Unable to update checksum:{}", e.getMessage( ) );
207            if (throwExceptions) {
208                if (e instanceof FileNotFoundException) {
209                    throw new ChecksumValidationException(ChecksumValidationException.ValidationError.FILE_NOT_FOUND, e);
210                } else {
211                    throw new ChecksumValidationException(ChecksumValidationException.ValidationError.READ_ERROR, e);
212                }
213            } else {
214                return false;
215            }
216        }
217
218        boolean valid = true;
219        boolean fileExists = false;
220
221        // No file exists -> return false
222        // if at least one file exists:
223        // -> all existing files must be valid
224
225        // check the checksum files
226        try
227        {
228
229            for ( Checksum checksum : checksums )
230            {
231                ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm( );
232                Path checksumFile = getChecksumFile( checksumAlgorithm );
233
234                if (Files.exists(checksumFile)) {
235                    fileExists = true;
236                    String expectedChecksum = parseChecksum(checksumFile, checksumAlgorithm, referenceFile.getFileName().toString(), FILE_ENCODING);
237
238                    valid &= checksum.compare(expectedChecksum);
239                }
240            }
241        }
242        catch ( ChecksumValidationException e )
243        {
244            log.warn( "Unable to read / parse checksum: {}", e.getMessage( ) );
245            if (throwExceptions) {
246                throw e;
247            } else
248            {
249                return false;
250            }
251        }
252
253        return fileExists && valid;
254    }
255
256    public Path getReferenceFile( )
257    {
258        return referenceFile;
259    }
260
261
262
263    public UpdateStatusList fixChecksum(ChecksumAlgorithm algorithm) {
264        return fixChecksums( Arrays.asList(algorithm) );
265    }
266
267    /**
268     * Writes a checksum file, if it does not exist or if it exists and has a different
269     * checksum value.
270     *
271     * @param algorithms the hashes to check for.
272     * @return true if checksums were created successfully.
273     */
274    public UpdateStatusList fixChecksums( List<ChecksumAlgorithm> algorithms )
275    {
276        UpdateStatusList result = UpdateStatusList.INITIALIZE(algorithms);
277        List<Checksum> checksums;
278
279
280        try
281        {
282            // Parse file once, for all checksums.
283            checksums = ChecksumUtil.initializeChecksums(getReferenceFile(), algorithms);
284        }
285        catch (IOException e )
286        {
287            log.warn( e.getMessage( ), e );
288            result.setTotalError(e);
289            return result;
290        }
291        // Any checksums?
292        if ( checksums.isEmpty( ) )
293        {
294            // No checksum objects, no checksum files, default to is valid.
295            return result;
296        }
297
298        boolean valid = true;
299
300        // check the hash files
301        for ( Checksum checksum : checksums )
302        {
303            ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm( );
304            try
305            {
306                Path checksumFile = getChecksumFile( checksumAlgorithm );
307                if ( Files.exists( checksumFile ) )
308                {
309                    String expectedChecksum;
310                    try
311                    {
312                        expectedChecksum = parseChecksum( checksumFile, checksumAlgorithm, referenceFile.getFileName( ).toString( ), FILE_ENCODING );
313                    } catch (ChecksumValidationException ex) {
314                        expectedChecksum = "";
315                    }
316
317                    if ( !checksum.compare( expectedChecksum ) )
318                    {
319                        // overwrite checksum file
320                        writeChecksumFile( checksumFile, FILE_ENCODING, checksum.getChecksum( ) );
321                        result.setStatus(checksumAlgorithm,UpdateStatus.UPDATED);
322                    }
323                }
324                else
325                {
326                    writeChecksumFile( checksumFile, FILE_ENCODING, checksum.getChecksum( ) );
327                    result.setStatus(checksumAlgorithm, UpdateStatus.CREATED);
328                }
329            }
330            catch ( ChecksumValidationException e )
331            {
332                log.warn( e.getMessage( ), e );
333                result.setErrorStatus(checksumAlgorithm, e);
334            }
335        }
336
337        return result;
338
339    }
340
341    private void writeChecksumFile( Path checksumFile, Charset encoding, String checksumHex )
342    {
343        FileUtils.writeStringToFile( checksumFile, FILE_ENCODING, checksumHex + "  " + referenceFile.getFileName( ).toString( ) );
344    }
345
346    private boolean isValidChecksumPattern( String filename, String path )
347    {
348        // check if it is a remote metadata file
349
350        Matcher m = METADATA_PATTERN.matcher( path );
351        if ( m.matches( ) )
352        {
353            return filename.endsWith( path ) || ( "-".equals( filename ) ) || filename.endsWith( "maven-metadata.xml" );
354        }
355
356        return filename.endsWith( path ) || ( "-".equals( filename ) );
357    }
358
359    /**
360     * Parse a checksum string.
361     * <p>
362     * Validate the expected path, and expected checksum algorithm, then return
363     * the trimmed checksum hex string.
364     * </p>
365     *
366     * @param checksumFile The file where the checksum is stored
367     * @param checksumAlgorithm The checksum algorithm to check
368     * @param fileName The filename of the reference file
369     * @return
370     * @throws IOException
371     */
372    public String parseChecksum( Path checksumFile, ChecksumAlgorithm checksumAlgorithm, String fileName, Charset encoding )
373        throws ChecksumValidationException
374    {
375        ChecksumFileContent fc = parseChecksumFile( checksumFile, checksumAlgorithm, encoding );
376        if ( fc.isFormatMatch() && !isValidChecksumPattern( fc.getFileReference( ), fileName ) )
377        {
378            throw new ChecksumValidationException(BAD_CHECKSUM_FILE_REF,
379                "The file reference '" + fc.getFileReference( ) + "' in the checksum file does not match expected file: '" + fileName + "'" );
380        } else if (!fc.isFormatMatch()) {
381            throw new ChecksumValidationException( BAD_CHECKSUM_FILE, "The checksum file content could not be parsed: "+checksumFile );
382        }
383        return fc.getChecksum( );
384
385    }
386    public ChecksumFileContent parseChecksumFile( Path checksumFile, ChecksumAlgorithm checksumAlgorithm, Charset encoding )
387    {
388        ChecksumFileContent fc = new ChecksumFileContent( );
389        String rawChecksumString = FileUtils.readFileToString( checksumFile, encoding );
390        String trimmedChecksum = rawChecksumString.replace( '\n', ' ' ).trim( );
391
392        // Free-BSD / openssl
393        String regex = checksumAlgorithm.getType( ) + "\\s*\\(([^)]*)\\)\\s*=\\s*([a-fA-F0-9]+)";
394        Matcher m = Pattern.compile( regex ).matcher( trimmedChecksum );
395        if ( m.matches( ) )
396        {
397            fc.setFileReference( m.group( 1 ) );
398            fc.setChecksum( m.group( 2 ) );
399            fc.setFormatMatch( true );
400        }
401        else
402        {
403            // GNU tools
404            m = Pattern.compile( "([a-fA-F0-9]+)\\s+\\*?(.+)" ).matcher( trimmedChecksum );
405            if ( m.matches( ) )
406            {
407                fc.setFileReference( m.group( 2 ) );
408                fc.setChecksum( m.group( 1 ) );
409                fc.setFormatMatch( true );
410            }
411            else
412            {
413                fc.setFileReference( "" );
414                fc.setChecksum( trimmedChecksum );
415                fc.setFormatMatch( false );
416            }
417        }
418        return fc;
419    }
420}