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.internal.impl.checksum;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.BufferedReader;
26  import java.io.IOException;
27  import java.io.UncheckedIOException;
28  import java.nio.charset.StandardCharsets;
29  import java.nio.file.Files;
30  import java.nio.file.Path;
31  import java.util.ArrayList;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Objects;
36  import java.util.concurrent.ConcurrentHashMap;
37  import java.util.concurrent.atomic.AtomicBoolean;
38  
39  import org.eclipse.aether.MultiRuntimeException;
40  import org.eclipse.aether.RepositorySystemSession;
41  import org.eclipse.aether.artifact.Artifact;
42  import org.eclipse.aether.impl.RepositorySystemLifecycle;
43  import org.eclipse.aether.internal.impl.LocalPathComposer;
44  import org.eclipse.aether.repository.ArtifactRepository;
45  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
46  import org.eclipse.aether.util.FileUtils;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  import static java.util.Objects.requireNonNull;
51  import static java.util.stream.Collectors.toList;
52  
53  /**
54   * Compact file {@link FileTrustedChecksumsSourceSupport} implementation that use specified directory as base
55   * directory, where it expects a "summary" file named as "checksums.${checksumExt}" for each checksum algorithm.
56   * File format is GNU Coreutils compatible: each line holds checksum followed by two spaces and artifact relative path
57   * (from local repository root, without leading "./"). This means that trusted checksums summary file can be used to
58   * validate artifacts or generate it using standard GNU tools like GNU {@code sha1sum} is (for BSD derivatives same
59   * file can be used with {@code -r} switch).
60   * <p>
61   * The format supports comments "#" (hash) and empty lines for easier structuring the file content, and both are
62   * ignored. Also, their presence makes the summary file incompatible with GNU Coreutils format. On save of the
63   * summary file, the comments and empty lines are lost, and file is sorted by path names for easier diffing
64   * (2nd column in file).
65   * <p>
66   * The source by default is "origin aware", and it will factor in origin repository ID as well into summary file name,
67   * for example "checksums-central.sha256".
68   * <p>
69   * Example commands for managing summary file (in examples will use repository ID "central"):
70   * <ul>
71   *     <li>To create summary file: {@code find * -not -name "checksums-central.sha256" -type f -print0 |
72   *       xargs -0 sha256sum | sort -k 2 > checksums-central.sha256}</li>
73   *     <li>To verify artifacts using summary file: {@code sha256sum --quiet -c checksums-central.sha256}</li>
74   * </ul>
75   * <p>
76   * The checksums summary file is lazily loaded and remains cached during lifetime of the component, so file changes
77   * during lifecycle of the component are not picked up. This implementation can be simultaneously used to lookup and
78   * also record checksums. The recorded checksums will become visible for every session, and will be flushed
79   * at repository system shutdown, merged with existing ones on disk.
80   * <p>
81   * The name of this implementation is "summaryFile".
82   *
83   * @see <a href="https://man7.org/linux/man-pages/man1/sha1sum.1.html">sha1sum man page</a>
84   * @see <a href="https://www.gnu.org/software/coreutils/manual/coreutils.html#md5sum-invocation">GNU Coreutils: md5sum</a>
85   * @since 1.9.0
86   */
87  @Singleton
88  @Named(SummaryFileTrustedChecksumsSource.NAME)
89  public final class SummaryFileTrustedChecksumsSource extends FileTrustedChecksumsSourceSupport {
90      public static final String NAME = "summaryFile";
91  
92      private static final String CHECKSUMS_FILE_PREFIX = "checksums";
93  
94      private static final Logger LOGGER = LoggerFactory.getLogger(SummaryFileTrustedChecksumsSource.class);
95  
96      private final LocalPathComposer localPathComposer;
97  
98      private final RepositorySystemLifecycle repositorySystemLifecycle;
99  
100     private final ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> checksums;
101 
102     private final ConcurrentHashMap<Path, Boolean> changedChecksums;
103 
104     private final AtomicBoolean onShutdownHandlerRegistered;
105 
106     @Inject
107     public SummaryFileTrustedChecksumsSource(
108             LocalPathComposer localPathComposer, RepositorySystemLifecycle repositorySystemLifecycle) {
109         super(NAME);
110         this.localPathComposer = requireNonNull(localPathComposer);
111         this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle);
112         this.checksums = new ConcurrentHashMap<>();
113         this.changedChecksums = new ConcurrentHashMap<>();
114         this.onShutdownHandlerRegistered = new AtomicBoolean(false);
115     }
116 
117     @Override
118     protected Map<String, String> doGetTrustedArtifactChecksums(
119             RepositorySystemSession session,
120             Artifact artifact,
121             ArtifactRepository artifactRepository,
122             List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
123         final HashMap<String, String> result = new HashMap<>();
124         final Path basedir = getBasedir(session, false);
125         if (Files.isDirectory(basedir)) {
126             final String artifactPath = localPathComposer.getPathForArtifact(artifact, false);
127             final boolean originAware = isOriginAware(session);
128             for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
129                 Path summaryFile = summaryFile(
130                         basedir, originAware, artifactRepository.getId(), checksumAlgorithmFactory.getFileExtension());
131                 ConcurrentHashMap<String, String> algorithmChecksums =
132                         checksums.computeIfAbsent(summaryFile, f -> loadProvidedChecksums(summaryFile));
133                 String checksum = algorithmChecksums.get(artifactPath);
134                 if (checksum != null) {
135                     result.put(checksumAlgorithmFactory.getName(), checksum);
136                 }
137             }
138         }
139         return result;
140     }
141 
142     @Override
143     protected SummaryFileWriter doGetTrustedArtifactChecksumsWriter(RepositorySystemSession session) {
144         if (onShutdownHandlerRegistered.compareAndSet(false, true)) {
145             repositorySystemLifecycle.addOnSystemEndedHandler(this::saveRecordedLines);
146         }
147         return new SummaryFileWriter(checksums, getBasedir(session, true), isOriginAware(session));
148     }
149 
150     /**
151      * Returns the summary file path. The file itself and its parent directories may not exist, this method merely
152      * calculate the path.
153      */
154     private Path summaryFile(Path basedir, boolean originAware, String repositoryId, String checksumExtension) {
155         String fileName = CHECKSUMS_FILE_PREFIX;
156         if (originAware) {
157             fileName += "-" + repositoryId;
158         }
159         return basedir.resolve(fileName + "." + checksumExtension);
160     }
161 
162     private ConcurrentHashMap<String, String> loadProvidedChecksums(Path summaryFile) {
163         ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>();
164         if (Files.isRegularFile(summaryFile)) {
165             try (BufferedReader reader = Files.newBufferedReader(summaryFile, StandardCharsets.UTF_8)) {
166                 String line;
167                 while ((line = reader.readLine()) != null) {
168                     if (!line.startsWith("#") && !line.isEmpty()) {
169                         String[] parts = line.split("  ", 2);
170                         if (parts.length == 2) {
171                             String newChecksum = parts[0];
172                             String artifactPath = parts[1];
173                             String oldChecksum = result.put(artifactPath, newChecksum);
174                             if (oldChecksum != null) {
175                                 if (Objects.equals(oldChecksum, newChecksum)) {
176                                     LOGGER.warn(
177                                             "Checksums file '{}' contains duplicate checksums for artifact {}: {}",
178                                             summaryFile,
179                                             artifactPath,
180                                             oldChecksum);
181                                 } else {
182                                     LOGGER.warn(
183                                             "Checksums file '{}' contains different checksums for artifact {}: "
184                                                     + "old '{}' replaced by new '{}'",
185                                             summaryFile,
186                                             artifactPath,
187                                             oldChecksum,
188                                             newChecksum);
189                                 }
190                             }
191                         } else {
192                             LOGGER.warn("Checksums file '{}' ignored malformed line '{}'", summaryFile, line);
193                         }
194                     }
195                 }
196             } catch (IOException e) {
197                 throw new UncheckedIOException(e);
198             }
199             LOGGER.info("Loaded {} trusted checksums from {}", result.size(), summaryFile);
200         }
201         return result;
202     }
203 
204     private class SummaryFileWriter implements Writer {
205         private final ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache;
206 
207         private final Path basedir;
208 
209         private final boolean originAware;
210 
211         private SummaryFileWriter(
212                 ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache, Path basedir, boolean originAware) {
213             this.cache = cache;
214             this.basedir = basedir;
215             this.originAware = originAware;
216         }
217 
218         @Override
219         public void addTrustedArtifactChecksums(
220                 Artifact artifact,
221                 ArtifactRepository artifactRepository,
222                 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
223                 Map<String, String> trustedArtifactChecksums) {
224             String artifactPath = localPathComposer.getPathForArtifact(artifact, false);
225             for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
226                 Path summaryFile = summaryFile(
227                         basedir, originAware, artifactRepository.getId(), checksumAlgorithmFactory.getFileExtension());
228                 String checksum = requireNonNull(trustedArtifactChecksums.get(checksumAlgorithmFactory.getName()));
229 
230                 String oldChecksum = cache.computeIfAbsent(summaryFile, k -> loadProvidedChecksums(summaryFile))
231                         .put(artifactPath, checksum);
232 
233                 if (oldChecksum == null) {
234                     changedChecksums.put(summaryFile, Boolean.TRUE); // new
235                 } else if (!Objects.equals(oldChecksum, checksum)) {
236                     changedChecksums.put(summaryFile, Boolean.TRUE); // replaced
237                     LOGGER.info(
238                             "Trusted checksum for artifact {} replaced: old {}, new {}",
239                             artifact,
240                             oldChecksum,
241                             checksum);
242                 }
243             }
244         }
245     }
246 
247     /**
248      * On-close handler that saves recorded checksums, if any.
249      */
250     private void saveRecordedLines() {
251         if (changedChecksums.isEmpty()) {
252             return;
253         }
254 
255         ArrayList<Exception> exceptions = new ArrayList<>();
256         for (Map.Entry<Path, ConcurrentHashMap<String, String>> entry : checksums.entrySet()) {
257             Path summaryFile = entry.getKey();
258             if (changedChecksums.get(summaryFile) != Boolean.TRUE) {
259                 continue;
260             }
261             ConcurrentHashMap<String, String> recordedLines = entry.getValue();
262             if (!recordedLines.isEmpty()) {
263                 try {
264                     ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>();
265                     result.putAll(loadProvidedChecksums(summaryFile));
266                     result.putAll(recordedLines);
267 
268                     LOGGER.info("Saving {} checksums to '{}'", result.size(), summaryFile);
269                     FileUtils.writeFileWithBackup(
270                             summaryFile,
271                             p -> Files.write(
272                                     p,
273                                     result.entrySet().stream()
274                                             .sorted(Map.Entry.comparingByValue())
275                                             .map(e -> e.getValue() + "  " + e.getKey())
276                                             .collect(toList())));
277                 } catch (IOException e) {
278                     exceptions.add(e);
279                 }
280             }
281         }
282         MultiRuntimeException.mayThrow("session save checksums failure", exceptions);
283     }
284 }