This project has retired. For details please refer to its Attic page.
ChecksummedFile xref
View Javadoc
1   package org.apache.archiva.checksum;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.archiva.common.utils.FileUtils;
23  import org.slf4j.Logger;
24  import org.slf4j.LoggerFactory;
25  
26  import java.io.FileNotFoundException;
27  import java.io.IOException;
28  import java.nio.charset.Charset;
29  import java.nio.file.Files;
30  import java.nio.file.Path;
31  import java.nio.file.StandardOpenOption;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.List;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  
38  import static org.apache.archiva.checksum.ChecksumValidationException.ValidationError.BAD_CHECKSUM_FILE;
39  import static org.apache.archiva.checksum.ChecksumValidationException.ValidationError.BAD_CHECKSUM_FILE_REF;
40  
41  /**
42   * ChecksummedFile
43   * <p>Terminology:</p>
44   * <dl>
45   * <dt>Checksum File</dt>
46   * <dd>The file that contains the previously calculated checksum value for the reference file.
47   * This is a text file with the extension ".sha1" or ".md5", and contains a single entry
48   * consisting of an optional reference filename, and a checksum string.
49   * </dd>
50   * <dt>Reference File</dt>
51   * <dd>The file that is being referenced in the checksum file.</dd>
52   * </dl>
53   */
54  public class ChecksummedFile
55  {
56  
57      private static Charset FILE_ENCODING = Charset.forName( "UTF-8" );
58  
59      private final Logger log = LoggerFactory.getLogger( ChecksummedFile.class );
60  
61      private static final Pattern METADATA_PATTERN = Pattern.compile( "maven-metadata-\\S*.xml" );
62  
63      private final Path referenceFile;
64  
65      /**
66       * Construct a ChecksummedFile object.
67       *
68       * @param referenceFile
69       */
70      public ChecksummedFile( final Path referenceFile )
71      {
72          this.referenceFile = referenceFile;
73      }
74  
75  
76      public static ChecksumReference getFromChecksumFile( Path checksumFile )
77      {
78          ChecksumAlgorithm alg = ChecksumAlgorithm.getByExtension( checksumFile );
79          ChecksummedFilemmedFile.html#ChecksummedFile">ChecksummedFile file = new ChecksummedFile( getReferenceFile( checksumFile ) );
80          return new ChecksumReference( file, alg, checksumFile );
81      }
82  
83      private static Path getReferenceFile( Path checksumFile )
84      {
85          String fileName = checksumFile.getFileName( ).toString( );
86          return checksumFile.resolveSibling( fileName.substring( 0, fileName.lastIndexOf( '.' ) ) );
87      }
88  
89      /**
90       * Calculate the checksum based on a given checksum.
91       *
92       * @param checksumAlgorithm the algorithm to use.
93       * @return the checksum string for the file.
94       * @throws IOException if unable to calculate the checksum.
95       */
96      public String calculateChecksum( ChecksumAlgorithm checksumAlgorithm )
97          throws IOException
98      {
99  
100         Checksumksum.html#Checksum">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         ChecksumFileContentileContent.html#ChecksumFileContent">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 }