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 }