View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.connector.basic;
20  
21  import java.io.IOException;
22  import java.io.UncheckedIOException;
23  import java.net.URI;
24  import java.nio.file.Path;
25  import java.util.Collection;
26  import java.util.HashMap;
27  import java.util.Map;
28  
29  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
30  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
31  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy.ChecksumKind;
32  import org.eclipse.aether.spi.connector.layout.RepositoryLayout.ChecksumLocation;
33  import org.eclipse.aether.spi.io.ChecksumProcessor;
34  import org.eclipse.aether.spi.io.PathProcessor;
35  import org.eclipse.aether.transfer.ChecksumFailureException;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  /**
40   * Performs checksum validation for a downloaded file.
41   */
42  final class ChecksumValidator {
43  
44      interface ChecksumFetcher {
45  
46          /**
47           * Fetches the checksums from remote location into provided local file. The checksums fetched in this way
48           * are of kind {@link ChecksumKind#REMOTE_EXTERNAL}.
49           */
50          boolean fetchChecksum(URI remote, Path local) throws Exception;
51      }
52  
53      private static final Logger LOGGER = LoggerFactory.getLogger(ChecksumValidator.class);
54  
55      private final Path dataPath;
56  
57      private final Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories;
58  
59      private final PathProcessor pathProcessor;
60  
61      private final ChecksumProcessor checksumProcessor;
62  
63      private final ChecksumFetcher checksumFetcher;
64  
65      private final ChecksumPolicy checksumPolicy;
66  
67      private final Map<String, String> providedChecksums;
68  
69      private final Collection<ChecksumLocation> checksumLocations;
70  
71      private final Map<Path, String> checksumExpectedValues;
72  
73      @SuppressWarnings("checkstyle:parameternumber")
74      ChecksumValidator(
75              Path dataPath,
76              Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
77              PathProcessor pathProcessor,
78              ChecksumProcessor checksumProcessor,
79              ChecksumFetcher checksumFetcher,
80              ChecksumPolicy checksumPolicy,
81              Map<String, String> providedChecksums,
82              Collection<ChecksumLocation> checksumLocations) {
83          this.dataPath = dataPath;
84          this.checksumAlgorithmFactories = checksumAlgorithmFactories;
85          this.pathProcessor = pathProcessor;
86          this.checksumProcessor = checksumProcessor;
87          this.checksumFetcher = checksumFetcher;
88          this.checksumPolicy = checksumPolicy;
89          this.providedChecksums = providedChecksums;
90          this.checksumLocations = checksumLocations;
91          this.checksumExpectedValues = new HashMap<>();
92      }
93  
94      public ChecksumCalculator newChecksumCalculator(Path targetFile) {
95          if (checksumPolicy != null) {
96              return ChecksumCalculator.newInstance(targetFile, checksumAlgorithmFactories);
97          }
98          return null;
99      }
100 
101     public void validate(Map<String, ?> actualChecksums, Map<String, ?> includedChecksums)
102             throws ChecksumFailureException {
103         if (checksumPolicy == null) {
104             return;
105         }
106         if (providedChecksums != null && validateChecksums(actualChecksums, ChecksumKind.PROVIDED, providedChecksums)) {
107             return;
108         }
109         if (includedChecksums != null
110                 && validateChecksums(actualChecksums, ChecksumKind.REMOTE_INCLUDED, includedChecksums)) {
111             return;
112         }
113         if (!checksumLocations.isEmpty()) {
114             if (validateExternalChecksums(actualChecksums)) {
115                 return;
116             }
117             checksumPolicy.onNoMoreChecksums();
118         }
119     }
120 
121     private boolean validateChecksums(Map<String, ?> actualChecksums, ChecksumKind kind, Map<String, ?> checksums)
122             throws ChecksumFailureException {
123         for (Map.Entry<String, ?> entry : checksums.entrySet()) {
124             String algo = entry.getKey();
125             Object calculated = actualChecksums.get(algo);
126             if (!(calculated instanceof String)) {
127                 continue;
128             }
129             ChecksumAlgorithmFactory checksumAlgorithmFactory = checksumAlgorithmFactories.stream()
130                     .filter(a -> a.getName().equals(algo))
131                     .findFirst()
132                     .orElse(null);
133             if (checksumAlgorithmFactory == null) {
134                 continue;
135             }
136 
137             String actual = String.valueOf(calculated);
138             String expected = entry.getValue().toString();
139             checksumExpectedValues.put(getChecksumPath(checksumAlgorithmFactory), expected);
140 
141             if (!isEqualChecksum(expected, actual)) {
142                 checksumPolicy.onChecksumMismatch(
143                         checksumAlgorithmFactory.getName(),
144                         kind,
145                         new ChecksumFailureException(expected, kind.name(), actual));
146             } else if (checksumPolicy.onChecksumMatch(checksumAlgorithmFactory.getName(), kind)) {
147                 return true;
148             }
149         }
150         return false;
151     }
152 
153     private boolean validateExternalChecksums(Map<String, ?> actualChecksums) throws ChecksumFailureException {
154         for (ChecksumLocation checksumLocation : checksumLocations) {
155             ChecksumAlgorithmFactory factory = checksumLocation.getChecksumAlgorithmFactory();
156             Object calculated = actualChecksums.get(factory.getName());
157             if (calculated instanceof Exception) {
158                 checksumPolicy.onChecksumError(
159                         factory.getName(), ChecksumKind.REMOTE_EXTERNAL, new ChecksumFailureException((Exception)
160                                 calculated));
161                 continue;
162             }
163             Path checksumFile = getChecksumPath(checksumLocation.getChecksumAlgorithmFactory());
164             try (PathProcessor.TempFile tempFile = pathProcessor.newTempFile()) {
165                 Path tmp = tempFile.getPath();
166                 try {
167                     if (!checksumFetcher.fetchChecksum(checksumLocation.getLocation(), tmp)) {
168                         continue;
169                     }
170                 } catch (Exception e) {
171                     checksumPolicy.onChecksumError(
172                             factory.getName(), ChecksumKind.REMOTE_EXTERNAL, new ChecksumFailureException(e));
173                     continue;
174                 }
175 
176                 String actual = String.valueOf(calculated);
177                 String expected = checksumProcessor.readChecksum(tmp);
178                 checksumExpectedValues.put(checksumFile, expected);
179 
180                 if (!isEqualChecksum(expected, actual)) {
181                     checksumPolicy.onChecksumMismatch(
182                             factory.getName(),
183                             ChecksumKind.REMOTE_EXTERNAL,
184                             new ChecksumFailureException(expected, ChecksumKind.REMOTE_EXTERNAL.name(), actual));
185                 } else if (checksumPolicy.onChecksumMatch(factory.getName(), ChecksumKind.REMOTE_EXTERNAL)) {
186                     return true;
187                 }
188             } catch (IOException e) {
189                 checksumPolicy.onChecksumError(
190                         factory.getName(), ChecksumKind.REMOTE_EXTERNAL, new ChecksumFailureException(e));
191             }
192         }
193         return false;
194     }
195 
196     private static boolean isEqualChecksum(String expected, String actual) {
197         return expected.equalsIgnoreCase(actual);
198     }
199 
200     private Path getChecksumPath(ChecksumAlgorithmFactory factory) {
201         return dataPath.getParent().resolve(dataPath.getFileName() + "." + factory.getFileExtension());
202     }
203 
204     public void retry() {
205         checksumPolicy.onTransferRetry();
206         checksumExpectedValues.clear();
207     }
208 
209     public boolean handle(ChecksumFailureException exception) {
210         return checksumPolicy.onTransferChecksumFailure(exception);
211     }
212 
213     public void commit() {
214         for (Map.Entry<Path, String> entry : checksumExpectedValues.entrySet()) {
215             Path checksumFile = entry.getKey();
216             try {
217                 checksumProcessor.writeChecksum(checksumFile, entry.getValue());
218             } catch (IOException e) {
219                 LOGGER.debug("Failed to write checksum file {}", checksumFile, e);
220                 throw new UncheckedIOException(e);
221             }
222         }
223         checksumExpectedValues.clear();
224     }
225 }