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.transfer.ChecksumFailureException;
35  import org.eclipse.aether.util.FileUtils;
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 ChecksumProcessor checksumProcessor;
60  
61      private final ChecksumFetcher checksumFetcher;
62  
63      private final ChecksumPolicy checksumPolicy;
64  
65      private final Map<String, String> providedChecksums;
66  
67      private final Collection<ChecksumLocation> checksumLocations;
68  
69      private final Map<Path, String> checksumExpectedValues;
70  
71      ChecksumValidator(
72              Path dataPath,
73              Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
74              ChecksumProcessor checksumProcessor,
75              ChecksumFetcher checksumFetcher,
76              ChecksumPolicy checksumPolicy,
77              Map<String, String> providedChecksums,
78              Collection<ChecksumLocation> checksumLocations) {
79          this.dataPath = dataPath;
80          this.checksumAlgorithmFactories = checksumAlgorithmFactories;
81          this.checksumProcessor = checksumProcessor;
82          this.checksumFetcher = checksumFetcher;
83          this.checksumPolicy = checksumPolicy;
84          this.providedChecksums = providedChecksums;
85          this.checksumLocations = checksumLocations;
86          this.checksumExpectedValues = new HashMap<>();
87      }
88  
89      public ChecksumCalculator newChecksumCalculator(Path targetFile) {
90          if (checksumPolicy != null) {
91              return ChecksumCalculator.newInstance(targetFile, checksumAlgorithmFactories);
92          }
93          return null;
94      }
95  
96      public void validate(Map<String, ?> actualChecksums, Map<String, ?> includedChecksums)
97              throws ChecksumFailureException {
98          if (checksumPolicy == null) {
99              return;
100         }
101         if (providedChecksums != null && validateChecksums(actualChecksums, ChecksumKind.PROVIDED, providedChecksums)) {
102             return;
103         }
104         if (includedChecksums != null
105                 && validateChecksums(actualChecksums, ChecksumKind.REMOTE_INCLUDED, includedChecksums)) {
106             return;
107         }
108         if (!checksumLocations.isEmpty()) {
109             if (validateExternalChecksums(actualChecksums)) {
110                 return;
111             }
112             checksumPolicy.onNoMoreChecksums();
113         }
114     }
115 
116     private boolean validateChecksums(Map<String, ?> actualChecksums, ChecksumKind kind, Map<String, ?> checksums)
117             throws ChecksumFailureException {
118         for (Map.Entry<String, ?> entry : checksums.entrySet()) {
119             String algo = entry.getKey();
120             Object calculated = actualChecksums.get(algo);
121             if (!(calculated instanceof String)) {
122                 continue;
123             }
124             ChecksumAlgorithmFactory checksumAlgorithmFactory = checksumAlgorithmFactories.stream()
125                     .filter(a -> a.getName().equals(algo))
126                     .findFirst()
127                     .orElse(null);
128             if (checksumAlgorithmFactory == null) {
129                 continue;
130             }
131 
132             String actual = String.valueOf(calculated);
133             String expected = entry.getValue().toString();
134             checksumExpectedValues.put(getChecksumPath(checksumAlgorithmFactory), expected);
135 
136             if (!isEqualChecksum(expected, actual)) {
137                 checksumPolicy.onChecksumMismatch(
138                         checksumAlgorithmFactory.getName(),
139                         kind,
140                         new ChecksumFailureException(expected, kind.name(), actual));
141             } else if (checksumPolicy.onChecksumMatch(checksumAlgorithmFactory.getName(), kind)) {
142                 return true;
143             }
144         }
145         return false;
146     }
147 
148     private boolean validateExternalChecksums(Map<String, ?> actualChecksums) throws ChecksumFailureException {
149         for (ChecksumLocation checksumLocation : checksumLocations) {
150             ChecksumAlgorithmFactory factory = checksumLocation.getChecksumAlgorithmFactory();
151             Object calculated = actualChecksums.get(factory.getName());
152             if (calculated instanceof Exception) {
153                 checksumPolicy.onChecksumError(
154                         factory.getName(), ChecksumKind.REMOTE_EXTERNAL, new ChecksumFailureException((Exception)
155                                 calculated));
156                 continue;
157             }
158             Path checksumFile = getChecksumPath(checksumLocation.getChecksumAlgorithmFactory());
159             try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) {
160                 Path tmp = tempFile.getPath();
161                 try {
162                     if (!checksumFetcher.fetchChecksum(checksumLocation.getLocation(), tmp)) {
163                         continue;
164                     }
165                 } catch (Exception e) {
166                     checksumPolicy.onChecksumError(
167                             factory.getName(), ChecksumKind.REMOTE_EXTERNAL, new ChecksumFailureException(e));
168                     continue;
169                 }
170 
171                 String actual = String.valueOf(calculated);
172                 String expected = checksumProcessor.readChecksum(tmp);
173                 checksumExpectedValues.put(checksumFile, expected);
174 
175                 if (!isEqualChecksum(expected, actual)) {
176                     checksumPolicy.onChecksumMismatch(
177                             factory.getName(),
178                             ChecksumKind.REMOTE_EXTERNAL,
179                             new ChecksumFailureException(expected, ChecksumKind.REMOTE_EXTERNAL.name(), actual));
180                 } else if (checksumPolicy.onChecksumMatch(factory.getName(), ChecksumKind.REMOTE_EXTERNAL)) {
181                     return true;
182                 }
183             } catch (IOException e) {
184                 checksumPolicy.onChecksumError(
185                         factory.getName(), ChecksumKind.REMOTE_EXTERNAL, new ChecksumFailureException(e));
186             }
187         }
188         return false;
189     }
190 
191     private static boolean isEqualChecksum(String expected, String actual) {
192         return expected.equalsIgnoreCase(actual);
193     }
194 
195     private Path getChecksumPath(ChecksumAlgorithmFactory factory) {
196         return dataPath.getParent().resolve(dataPath.getFileName() + "." + factory.getFileExtension());
197     }
198 
199     public void retry() {
200         checksumPolicy.onTransferRetry();
201         checksumExpectedValues.clear();
202     }
203 
204     public boolean handle(ChecksumFailureException exception) {
205         return checksumPolicy.onTransferChecksumFailure(exception);
206     }
207 
208     public void commit() {
209         for (Map.Entry<Path, String> entry : checksumExpectedValues.entrySet()) {
210             Path checksumFile = entry.getKey();
211             try {
212                 checksumProcessor.writeChecksum(checksumFile, entry.getValue());
213             } catch (IOException e) {
214                 LOGGER.debug("Failed to write checksum file {}", checksumFile, e);
215                 throw new UncheckedIOException(e);
216             }
217         }
218         checksumExpectedValues.clear();
219     }
220 }