001    package 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    
022    import org.apache.commons.io.FileUtils;
023    import org.apache.commons.io.IOUtils;
024    import org.apache.commons.lang.StringUtils;
025    import org.slf4j.Logger;
026    import org.slf4j.LoggerFactory;
027    
028    import java.io.File;
029    import java.io.FileInputStream;
030    import java.io.IOException;
031    import java.util.ArrayList;
032    import java.util.List;
033    import java.util.regex.Matcher;
034    import java.util.regex.Pattern;
035    
036    /**
037     * ChecksummedFile
038     * <p/>
039     * <dl>
040     * <lh>Terminology:</lh>
041     * <dt>Checksum File</dt>
042     * <dd>The file that contains the previously calculated checksum value for the reference file.
043     * This is a text file with the extension ".sha1" or ".md5", and contains a single entry
044     * consisting of an optional reference filename, and a checksum string.
045     * </dd>
046     * <dt>Reference File</dt>
047     * <dd>The file that is being referenced in the checksum file.</dd>
048     * </dl>
049     *
050     *
051     */
052    public class ChecksummedFile
053    {
054        private Logger log = LoggerFactory.getLogger( ChecksummedFile.class );
055    
056        private final File referenceFile;
057    
058        /**
059         * Construct a ChecksummedFile object.
060         *
061         * @param referenceFile
062         */
063        public ChecksummedFile( final File referenceFile )
064        {
065            this.referenceFile = referenceFile;
066        }
067    
068        /**
069         * Calculate the checksum based on a given checksum.
070         *
071         * @param checksumAlgorithm the algorithm to use.
072         * @return the checksum string for the file.
073         * @throws IOException if unable to calculate the checksum.
074         */
075        public String calculateChecksum( ChecksumAlgorithm checksumAlgorithm )
076            throws IOException
077        {
078            FileInputStream fis = null;
079            try
080            {
081                Checksum checksum = new Checksum( checksumAlgorithm );
082                fis = new FileInputStream( referenceFile );
083                checksum.update( fis );
084                return checksum.getChecksum();
085            }
086            finally
087            {
088                IOUtils.closeQuietly( fis );
089            }
090        }
091    
092        /**
093         * Creates a checksum file of the provided referenceFile.
094         *
095         * @param checksumAlgorithm the hash to use.
096         * @return the checksum File that was created.
097         * @throws IOException if there was a problem either reading the referenceFile, or writing the checksum file.
098         */
099        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    }