1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
14  
15  
16  
17  
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  import java.util.function.Function;
39  import java.util.stream.Collectors;
40  
41  import org.eclipse.aether.MultiRuntimeException;
42  import org.eclipse.aether.RepositorySystemSession;
43  import org.eclipse.aether.artifact.Artifact;
44  import org.eclipse.aether.impl.RepositorySystemLifecycle;
45  import org.eclipse.aether.internal.impl.LocalPathComposer;
46  import org.eclipse.aether.repository.ArtifactRepository;
47  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
48  import org.eclipse.aether.spi.io.PathProcessor;
49  import org.eclipse.aether.util.ConfigUtils;
50  import org.eclipse.aether.util.repository.RepositoryIdHelper;
51  import org.slf4j.Logger;
52  import org.slf4j.LoggerFactory;
53  
54  import static java.util.Objects.requireNonNull;
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
82  
83  
84  
85  
86  
87  
88  
89  
90  @Singleton
91  @Named(SummaryFileTrustedChecksumsSource.NAME)
92  public final class SummaryFileTrustedChecksumsSource extends FileTrustedChecksumsSourceSupport {
93      public static final String NAME = "summaryFile";
94  
95      private static final String CONFIG_PROPS_PREFIX =
96              FileTrustedChecksumsSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".";
97  
98      
99  
100 
101 
102 
103 
104 
105     public static final String CONFIG_PROP_ENABLED = FileTrustedChecksumsSourceSupport.CONFIG_PROPS_PREFIX + NAME;
106 
107     
108 
109 
110 
111 
112 
113 
114     public static final String CONFIG_PROP_BASEDIR = CONFIG_PROPS_PREFIX + "basedir";
115 
116     public static final String LOCAL_REPO_PREFIX_DIR = ".checksums";
117 
118     
119 
120 
121 
122 
123 
124 
125     public static final String CONFIG_PROP_ORIGIN_AWARE = CONFIG_PROPS_PREFIX + "originAware";
126 
127     public static final String CHECKSUMS_FILE_PREFIX = "checksums";
128 
129     private static final Logger LOGGER = LoggerFactory.getLogger(SummaryFileTrustedChecksumsSource.class);
130 
131     private final LocalPathComposer localPathComposer;
132 
133     private final RepositorySystemLifecycle repositorySystemLifecycle;
134 
135     private final PathProcessor pathProcessor;
136 
137     private final ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> checksums;
138 
139     private final ConcurrentHashMap<Path, Boolean> changedChecksums;
140 
141     private final AtomicBoolean onShutdownHandlerRegistered;
142 
143     @Inject
144     public SummaryFileTrustedChecksumsSource(
145             LocalPathComposer localPathComposer,
146             RepositorySystemLifecycle repositorySystemLifecycle,
147             PathProcessor pathProcessor) {
148         this.localPathComposer = requireNonNull(localPathComposer);
149         this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle);
150         this.pathProcessor = requireNonNull(pathProcessor);
151         this.checksums = new ConcurrentHashMap<>();
152         this.changedChecksums = new ConcurrentHashMap<>();
153         this.onShutdownHandlerRegistered = new AtomicBoolean(false);
154     }
155 
156     @Override
157     protected boolean isEnabled(RepositorySystemSession session) {
158         return ConfigUtils.getBoolean(session, false, CONFIG_PROP_ENABLED);
159     }
160 
161     private boolean isOriginAware(RepositorySystemSession session) {
162         return ConfigUtils.getBoolean(session, true, CONFIG_PROP_ORIGIN_AWARE);
163     }
164 
165     @Override
166     protected Map<String, String> doGetTrustedArtifactChecksums(
167             RepositorySystemSession session,
168             Artifact artifact,
169             ArtifactRepository artifactRepository,
170             List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
171         final HashMap<String, String> result = new HashMap<>();
172         final Path basedir = getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false);
173         if (Files.isDirectory(basedir)) {
174             final String artifactPath = localPathComposer.getPathForArtifact(artifact, false);
175             final boolean originAware = isOriginAware(session);
176             for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
177                 Path summaryFile = summaryFile(
178                         basedir,
179                         originAware,
180                         RepositoryIdHelper.cachedIdToPathSegment(session).apply(artifactRepository),
181                         checksumAlgorithmFactory.getFileExtension());
182                 ConcurrentHashMap<String, String> algorithmChecksums =
183                         checksums.computeIfAbsent(summaryFile, f -> loadProvidedChecksums(summaryFile));
184                 String checksum = algorithmChecksums.get(artifactPath);
185                 if (checksum != null) {
186                     result.put(checksumAlgorithmFactory.getName(), checksum);
187                 }
188             }
189         }
190         return result;
191     }
192 
193     @Override
194     protected Writer doGetTrustedArtifactChecksumsWriter(RepositorySystemSession session) {
195         if (onShutdownHandlerRegistered.compareAndSet(false, true)) {
196             repositorySystemLifecycle.addOnSystemEndedHandler(this::saveRecordedLines);
197         }
198         return new SummaryFileWriter(
199                 checksums,
200                 getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, true),
201                 isOriginAware(session),
202                 RepositoryIdHelper.cachedIdToPathSegment(session));
203     }
204 
205     
206 
207 
208 
209     private Path summaryFile(Path basedir, boolean originAware, String safeRepositoryId, String checksumExtension) {
210         String fileName = CHECKSUMS_FILE_PREFIX;
211         if (originAware) {
212             fileName += "-" + safeRepositoryId;
213         }
214         return basedir.resolve(fileName + "." + checksumExtension);
215     }
216 
217     private ConcurrentHashMap<String, String> loadProvidedChecksums(Path summaryFile) {
218         ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>();
219         if (Files.isRegularFile(summaryFile)) {
220             try (BufferedReader reader = Files.newBufferedReader(summaryFile, StandardCharsets.UTF_8)) {
221                 String line;
222                 while ((line = reader.readLine()) != null) {
223                     if (!line.startsWith("#") && !line.isEmpty()) {
224                         String[] parts = line.split("  ", 2);
225                         if (parts.length == 2) {
226                             String newChecksum = parts[0];
227                             String artifactPath = parts[1];
228                             String oldChecksum = result.put(artifactPath, newChecksum);
229                             if (oldChecksum != null) {
230                                 if (Objects.equals(oldChecksum, newChecksum)) {
231                                     LOGGER.warn(
232                                             "Checksums file '{}' contains duplicate checksums for artifact {}: {}",
233                                             summaryFile,
234                                             artifactPath,
235                                             oldChecksum);
236                                 } else {
237                                     LOGGER.warn(
238                                             "Checksums file '{}' contains different checksums for artifact {}: "
239                                                     + "old '{}' replaced by new '{}'",
240                                             summaryFile,
241                                             artifactPath,
242                                             oldChecksum,
243                                             newChecksum);
244                                 }
245                             }
246                         } else {
247                             LOGGER.warn("Checksums file '{}' ignored malformed line '{}'", summaryFile, line);
248                         }
249                     }
250                 }
251             } catch (IOException e) {
252                 throw new UncheckedIOException(e);
253             }
254             LOGGER.info("Loaded {} trusted checksums from {}", result.size(), summaryFile);
255         }
256         return result;
257     }
258 
259     private class SummaryFileWriter implements Writer {
260         private final ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache;
261 
262         private final Path basedir;
263 
264         private final boolean originAware;
265 
266         private final Function<ArtifactRepository, String> idToPathSegmentFunction;
267 
268         private SummaryFileWriter(
269                 ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache,
270                 Path basedir,
271                 boolean originAware,
272                 Function<ArtifactRepository, String> idToPathSegmentFunction) {
273             this.cache = cache;
274             this.basedir = basedir;
275             this.originAware = originAware;
276             this.idToPathSegmentFunction = idToPathSegmentFunction;
277         }
278 
279         @Override
280         public void addTrustedArtifactChecksums(
281                 Artifact artifact,
282                 ArtifactRepository artifactRepository,
283                 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
284                 Map<String, String> trustedArtifactChecksums) {
285             String artifactPath = localPathComposer.getPathForArtifact(artifact, false);
286             for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
287                 Path summaryFile = summaryFile(
288                         basedir,
289                         originAware,
290                         idToPathSegmentFunction.apply(artifactRepository),
291                         checksumAlgorithmFactory.getFileExtension());
292                 String checksum = requireNonNull(trustedArtifactChecksums.get(checksumAlgorithmFactory.getName()));
293 
294                 String oldChecksum = cache.computeIfAbsent(summaryFile, k -> loadProvidedChecksums(summaryFile))
295                         .put(artifactPath, checksum);
296 
297                 if (oldChecksum == null) {
298                     changedChecksums.put(summaryFile, Boolean.TRUE); 
299                 } else if (!Objects.equals(oldChecksum, checksum)) {
300                     changedChecksums.put(summaryFile, Boolean.TRUE); 
301                     LOGGER.info(
302                             "Trusted checksum for artifact {} replaced: old {}, new {}",
303                             artifact,
304                             oldChecksum,
305                             checksum);
306                 }
307             }
308         }
309     }
310 
311     
312 
313 
314     private void saveRecordedLines() {
315         if (changedChecksums.isEmpty()) {
316             return;
317         }
318 
319         ArrayList<Exception> exceptions = new ArrayList<>();
320         for (Map.Entry<Path, ConcurrentHashMap<String, String>> entry : checksums.entrySet()) {
321             Path summaryFile = entry.getKey();
322             if (changedChecksums.get(summaryFile) != Boolean.TRUE) {
323                 continue;
324             }
325             ConcurrentHashMap<String, String> recordedLines = entry.getValue();
326             if (!recordedLines.isEmpty()) {
327                 try {
328                     ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>();
329                     result.putAll(loadProvidedChecksums(summaryFile));
330                     result.putAll(recordedLines);
331 
332                     LOGGER.info("Saving {} checksums to '{}'", result.size(), summaryFile);
333                     pathProcessor.writeWithBackup(
334                             summaryFile,
335                             result.entrySet().stream()
336                                     .sorted(Map.Entry.comparingByKey())
337                                     .map(e -> e.getValue() + "  " + e.getKey())
338                                     .collect(Collectors.joining(System.lineSeparator())));
339                 } catch (IOException e) {
340                     exceptions.add(e);
341                 }
342             }
343         }
344         MultiRuntimeException.mayThrow("session save checksums failure", exceptions);
345     }
346 }