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.commons.io.FileUtils;
23  import org.apache.commons.io.IOUtils;
24  import org.apache.commons.lang.StringUtils;
25  import org.slf4j.Logger;
26  import org.slf4j.LoggerFactory;
27  
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.IOException;
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  
36  /**
37   * ChecksummedFile
38   * <p/>
39   * <dl>
40   * <lh>Terminology:</lh>
41   * <dt>Checksum File</dt>
42   * <dd>The file that contains the previously calculated checksum value for the reference file.
43   * This is a text file with the extension ".sha1" or ".md5", and contains a single entry
44   * consisting of an optional reference filename, and a checksum string.
45   * </dd>
46   * <dt>Reference File</dt>
47   * <dd>The file that is being referenced in the checksum file.</dd>
48   * </dl>
49   *
50   *
51   */
52  public class ChecksummedFile
53  {
54      private Logger log = LoggerFactory.getLogger( ChecksummedFile.class );
55  
56      private final File referenceFile;
57  
58      /**
59       * Construct a ChecksummedFile object.
60       *
61       * @param referenceFile
62       */
63      public ChecksummedFile( final File referenceFile )
64      {
65          this.referenceFile = referenceFile;
66      }
67  
68      /**
69       * Calculate the checksum based on a given checksum.
70       *
71       * @param checksumAlgorithm the algorithm to use.
72       * @return the checksum string for the file.
73       * @throws IOException if unable to calculate the checksum.
74       */
75      public String calculateChecksum( ChecksumAlgorithm checksumAlgorithm )
76          throws IOException
77      {
78          FileInputStream fis = null;
79          try
80          {
81              Checksum checksum = new Checksum( checksumAlgorithm );
82              fis = new FileInputStream( referenceFile );
83              checksum.update( fis );
84              return checksum.getChecksum();
85          }
86          finally
87          {
88              IOUtils.closeQuietly( fis );
89          }
90      }
91  
92      /**
93       * Creates a checksum file of the provided referenceFile.
94       *
95       * @param checksumAlgorithm the hash to use.
96       * @return the checksum File that was created.
97       * @throws IOException if there was a problem either reading the referenceFile, or writing the checksum file.
98       */
99      public File createChecksum( ChecksumAlgorithm checksumAlgorithm )
100         throws IOException
101     {
102         File checksumFile = new File( referenceFile.getAbsolutePath() + "." + checksumAlgorithm.getExt() );
103         String checksum = calculateChecksum( checksumAlgorithm );
104         FileUtils.writeStringToFile( checksumFile, checksum + "  " + referenceFile.getName() );
105         return checksumFile;
106     }
107 
108     /**
109      * Get the checksum file for the reference file and hash.
110      *
111      * @param checksumAlgorithm the hash that we are interested in.
112      * @return the checksum file to return
113      */
114     public File getChecksumFile( ChecksumAlgorithm checksumAlgorithm )
115     {
116         return new File( referenceFile.getAbsolutePath() + "." + checksumAlgorithm.getExt() );
117     }
118 
119     /**
120      * <p>
121      * Given a checksum file, check to see if the file it represents is valid according to the checksum.
122      * </p>
123      * <p/>
124      * <p>
125      * NOTE: Only supports single file checksums of type MD5 or SHA1.
126      * </p>
127      *
128      * @param checksumFile the algorithms to check for.
129      * @return true if the checksum is valid for the file it represents. or if the checksum file does not exist.
130      * @throws IOException if the reading of the checksumFile or the file it refers to fails.
131      */
132     public boolean isValidChecksum( ChecksumAlgorithm algorithm )
133         throws IOException
134     {
135         return isValidChecksums( new ChecksumAlgorithm[]{ algorithm } );
136     }
137 
138     /**
139      * Of any checksum files present, validate that the reference file conforms
140      * the to the checksum.
141      *
142      * @param algorithms the algorithms to check for.
143      * @return true if the checksums report that the the reference file is valid, false if invalid.
144      */
145     public boolean isValidChecksums( ChecksumAlgorithm algorithms[] )
146     {
147         FileInputStream fis = null;
148         try
149         {
150             List<Checksum> checksums = new ArrayList<Checksum>( algorithms.length );
151             // Create checksum object for each algorithm.
152             for ( ChecksumAlgorithm checksumAlgorithm : algorithms )
153             {
154                 File checksumFile = getChecksumFile( checksumAlgorithm );
155 
156                 // Only add algorithm if checksum file exists.
157                 if ( checksumFile.exists() )
158                 {
159                     checksums.add( new Checksum( checksumAlgorithm ) );
160                 }
161             }
162 
163             // Any checksums?
164             if ( checksums.isEmpty() )
165             {
166                 // No checksum objects, no checksum files, default to is invalid.
167                 return false;
168             }
169 
170             // Parse file once, for all checksums.
171             try
172             {
173                 fis = new FileInputStream( referenceFile );
174                 Checksum.update( checksums, fis );
175             }
176             catch ( IOException e )
177             {
178                 log.warn( "Unable to update checksum:" + e.getMessage() );
179                 return false;
180             }
181 
182             boolean valid = true;
183 
184             // check the checksum files
185             try
186             {
187                 for ( Checksum checksum : checksums )
188                 {
189                     ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm();
190                     File checksumFile = getChecksumFile( checksumAlgorithm );
191 
192                     String rawChecksum = FileUtils.readFileToString( checksumFile );
193                     String expectedChecksum = parseChecksum( rawChecksum, checksumAlgorithm, referenceFile.getName() );
194 
195                     if ( !StringUtils.equalsIgnoreCase( expectedChecksum, checksum.getChecksum() ) )
196                     {
197                         valid = false;
198                     }
199                 }
200             }
201             catch ( IOException e )
202             {
203                 log.warn( "Unable to read / parse checksum: " + e.getMessage() );
204                 return false;
205             }
206 
207             return valid;
208         }
209         finally
210         {
211             IOUtils.closeQuietly( fis );
212         }
213     }
214 
215     /**
216      * Fix or create checksum files for the reference file.
217      *
218      * @param algorithms the hashes to check for.
219      * @return true if checksums were created successfully.
220      */
221     public boolean fixChecksums( ChecksumAlgorithm[] algorithms )
222     {
223         List<Checksum> checksums = new ArrayList<Checksum>( algorithms.length );
224         // Create checksum object for each algorithm.
225         for ( ChecksumAlgorithm checksumAlgorithm : algorithms )
226         {
227             checksums.add( new Checksum( checksumAlgorithm ) );
228         }
229 
230         // Any checksums?
231         if ( checksums.isEmpty() )
232         {
233             // No checksum objects, no checksum files, default to is valid.
234             return true;
235         }
236 
237         FileInputStream fis = null;
238         try
239         {
240             // Parse file once, for all checksums.
241             fis = new FileInputStream( referenceFile );
242             Checksum.update( checksums, fis );
243         }
244         catch ( IOException e )
245         {
246             log.warn( e.getMessage(), e );
247             return false;
248         }
249         finally
250         {
251             IOUtils.closeQuietly( fis );
252         }
253 
254         boolean valid = true;
255 
256         // check the hash files
257         for ( Checksum checksum : checksums )
258         {
259             ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm();
260             try
261             {
262                 File checksumFile = getChecksumFile( checksumAlgorithm );
263                 String actualChecksum = checksum.getChecksum();
264 
265                 if ( checksumFile.exists() )
266                 {
267                     String rawChecksum = FileUtils.readFileToString( checksumFile );
268                     String expectedChecksum = parseChecksum( rawChecksum, checksumAlgorithm, referenceFile.getName() );
269 
270                     if ( !StringUtils.equalsIgnoreCase( expectedChecksum, actualChecksum ) )
271                     {
272                         // create checksum (again)
273                         FileUtils.writeStringToFile( checksumFile, actualChecksum + "  " + referenceFile.getName() );
274                     }
275                 }
276                 else
277                 {
278                     FileUtils.writeStringToFile( checksumFile, actualChecksum + "  " + referenceFile.getName() );
279                 }
280             }
281             catch ( IOException e )
282             {
283                 log.warn( e.getMessage(), e );
284                 valid = false;
285             }
286         }
287 
288         return valid;
289 
290     }
291 
292     private boolean isValidChecksumPattern( String filename, String path )
293     {
294         // check if it is a remote metadata file
295         Pattern pattern = Pattern.compile( "maven-metadata-\\S*.xml" );
296         Matcher m = pattern.matcher( path );
297         if ( m.matches() )
298         {
299             return filename.endsWith( path ) || ( "-".equals( filename ) ) || filename.endsWith( "maven-metadata.xml" );
300         }
301 
302         return filename.endsWith( path ) || ( "-".equals( filename ) );
303     }
304 
305     /**
306      * Parse a checksum string.
307      * <p/>
308      * Validate the expected path, and expected checksum algorithm, then return
309      * the trimmed checksum hex string.
310      *
311      * @param rawChecksumString
312      * @param expectedHash
313      * @param expectedPath
314      * @return
315      * @throws IOException
316      */
317     public String parseChecksum( String rawChecksumString, ChecksumAlgorithm expectedHash, String expectedPath )
318         throws IOException
319     {
320         String trimmedChecksum = rawChecksumString.replace( '\n', ' ' ).trim();
321 
322         // Free-BSD / openssl
323         String regex = expectedHash.getType() + "\\s*\\(([^)]*)\\)\\s*=\\s*([a-fA-F0-9]+)";
324         Matcher m = Pattern.compile( regex ).matcher( trimmedChecksum );
325         if ( m.matches() )
326         {
327             String filename = m.group( 1 );
328             if ( !isValidChecksumPattern( filename, expectedPath ) )
329             {
330                 throw new IOException(
331                     "Supplied checksum file '" + filename + "' does not match expected file: '" + expectedPath + "'" );
332             }
333             trimmedChecksum = m.group( 2 );
334         }
335         else
336         {
337             // GNU tools
338             m = Pattern.compile( "([a-fA-F0-9]+)\\s+\\*?(.+)" ).matcher( trimmedChecksum );
339             if ( m.matches() )
340             {
341                 String filename = m.group( 2 );
342                 if ( !isValidChecksumPattern( filename, expectedPath ) )
343                 {
344                     throw new IOException(
345                         "Supplied checksum file '" + filename + "' does not match expected file: '" + expectedPath
346                             + "'" );
347                 }
348                 trimmedChecksum = m.group( 1 );
349             }
350         }
351         return trimmedChecksum;
352     }
353 }