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
40 import org.eclipse.aether.MultiRuntimeException;
41 import org.eclipse.aether.RepositorySystemSession;
42 import org.eclipse.aether.artifact.Artifact;
43 import org.eclipse.aether.impl.RepositorySystemLifecycle;
44 import org.eclipse.aether.internal.impl.LocalPathComposer;
45 import org.eclipse.aether.repository.ArtifactRepository;
46 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
47 import org.eclipse.aether.util.ConfigUtils;
48 import org.eclipse.aether.util.FileUtils;
49 import org.eclipse.aether.util.repository.RepositoryIdHelper;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import static java.util.Objects.requireNonNull;
54 import static java.util.stream.Collectors.toList;
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 ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> checksums;
136
137 private final ConcurrentHashMap<Path, Boolean> changedChecksums;
138
139 private final AtomicBoolean onShutdownHandlerRegistered;
140
141 @Inject
142 public SummaryFileTrustedChecksumsSource(
143 LocalPathComposer localPathComposer, RepositorySystemLifecycle repositorySystemLifecycle) {
144 this.localPathComposer = requireNonNull(localPathComposer);
145 this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle);
146 this.checksums = new ConcurrentHashMap<>();
147 this.changedChecksums = new ConcurrentHashMap<>();
148 this.onShutdownHandlerRegistered = new AtomicBoolean(false);
149 }
150
151 @Override
152 protected boolean isEnabled(RepositorySystemSession session) {
153 return ConfigUtils.getBoolean(session, false, CONFIG_PROP_ENABLED);
154 }
155
156 private boolean isOriginAware(RepositorySystemSession session) {
157 return ConfigUtils.getBoolean(session, true, CONFIG_PROP_ORIGIN_AWARE);
158 }
159
160 @Override
161 protected Map<String, String> doGetTrustedArtifactChecksums(
162 RepositorySystemSession session,
163 Artifact artifact,
164 ArtifactRepository artifactRepository,
165 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
166 final HashMap<String, String> result = new HashMap<>();
167 final Path basedir = getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false);
168 if (Files.isDirectory(basedir)) {
169 final String artifactPath = localPathComposer.getPathForArtifact(artifact, false);
170 final boolean originAware = isOriginAware(session);
171 for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
172 Path summaryFile = summaryFile(
173 basedir,
174 originAware,
175 RepositoryIdHelper.cachedIdToPathSegment(session).apply(artifactRepository),
176 checksumAlgorithmFactory.getFileExtension());
177 ConcurrentHashMap<String, String> algorithmChecksums =
178 checksums.computeIfAbsent(summaryFile, f -> loadProvidedChecksums(summaryFile));
179 String checksum = algorithmChecksums.get(artifactPath);
180 if (checksum != null) {
181 result.put(checksumAlgorithmFactory.getName(), checksum);
182 }
183 }
184 }
185 return result;
186 }
187
188 @Override
189 protected Writer doGetTrustedArtifactChecksumsWriter(RepositorySystemSession session) {
190 if (onShutdownHandlerRegistered.compareAndSet(false, true)) {
191 repositorySystemLifecycle.addOnSystemEndedHandler(this::saveRecordedLines);
192 }
193 return new SummaryFileWriter(
194 checksums,
195 getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, true),
196 isOriginAware(session),
197 RepositoryIdHelper.cachedIdToPathSegment(session));
198 }
199
200
201
202
203
204 private Path summaryFile(Path basedir, boolean originAware, String safeRepositoryId, String checksumExtension) {
205 String fileName = CHECKSUMS_FILE_PREFIX;
206 if (originAware) {
207 fileName += "-" + safeRepositoryId;
208 }
209 return basedir.resolve(fileName + "." + checksumExtension);
210 }
211
212 private ConcurrentHashMap<String, String> loadProvidedChecksums(Path summaryFile) {
213 ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>();
214 if (Files.isRegularFile(summaryFile)) {
215 try (BufferedReader reader = Files.newBufferedReader(summaryFile, StandardCharsets.UTF_8)) {
216 String line;
217 while ((line = reader.readLine()) != null) {
218 if (!line.startsWith("#") && !line.isEmpty()) {
219 String[] parts = line.split(" ", 2);
220 if (parts.length == 2) {
221 String newChecksum = parts[0];
222 String artifactPath = parts[1];
223 String oldChecksum = result.put(artifactPath, newChecksum);
224 if (oldChecksum != null) {
225 if (Objects.equals(oldChecksum, newChecksum)) {
226 LOGGER.warn(
227 "Checksums file '{}' contains duplicate checksums for artifact {}: {}",
228 summaryFile,
229 artifactPath,
230 oldChecksum);
231 } else {
232 LOGGER.warn(
233 "Checksums file '{}' contains different checksums for artifact {}: "
234 + "old '{}' replaced by new '{}'",
235 summaryFile,
236 artifactPath,
237 oldChecksum,
238 newChecksum);
239 }
240 }
241 } else {
242 LOGGER.warn("Checksums file '{}' ignored malformed line '{}'", summaryFile, line);
243 }
244 }
245 }
246 } catch (IOException e) {
247 throw new UncheckedIOException(e);
248 }
249 LOGGER.info("Loaded {} trusted checksums from {}", result.size(), summaryFile);
250 }
251 return result;
252 }
253
254 private class SummaryFileWriter implements Writer {
255 private final ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache;
256
257 private final Path basedir;
258
259 private final boolean originAware;
260
261 private final Function<ArtifactRepository, String> idToPathSegmentFunction;
262
263 private SummaryFileWriter(
264 ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache,
265 Path basedir,
266 boolean originAware,
267 Function<ArtifactRepository, String> idToPathSegmentFunction) {
268 this.cache = cache;
269 this.basedir = basedir;
270 this.originAware = originAware;
271 this.idToPathSegmentFunction = idToPathSegmentFunction;
272 }
273
274 @Override
275 public void addTrustedArtifactChecksums(
276 Artifact artifact,
277 ArtifactRepository artifactRepository,
278 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
279 Map<String, String> trustedArtifactChecksums) {
280 String artifactPath = localPathComposer.getPathForArtifact(artifact, false);
281 for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
282 Path summaryFile = summaryFile(
283 basedir,
284 originAware,
285 idToPathSegmentFunction.apply(artifactRepository),
286 checksumAlgorithmFactory.getFileExtension());
287 String checksum = requireNonNull(trustedArtifactChecksums.get(checksumAlgorithmFactory.getName()));
288
289 String oldChecksum = cache.computeIfAbsent(summaryFile, k -> loadProvidedChecksums(summaryFile))
290 .put(artifactPath, checksum);
291
292 if (oldChecksum == null) {
293 changedChecksums.put(summaryFile, Boolean.TRUE);
294 } else if (!Objects.equals(oldChecksum, checksum)) {
295 changedChecksums.put(summaryFile, Boolean.TRUE);
296 LOGGER.info(
297 "Trusted checksum for artifact {} replaced: old {}, new {}",
298 artifact,
299 oldChecksum,
300 checksum);
301 }
302 }
303 }
304 }
305
306
307
308
309 private void saveRecordedLines() {
310 if (changedChecksums.isEmpty()) {
311 return;
312 }
313
314 ArrayList<Exception> exceptions = new ArrayList<>();
315 for (Map.Entry<Path, ConcurrentHashMap<String, String>> entry : checksums.entrySet()) {
316 Path summaryFile = entry.getKey();
317 if (changedChecksums.get(summaryFile) != Boolean.TRUE) {
318 continue;
319 }
320 ConcurrentHashMap<String, String> recordedLines = entry.getValue();
321 if (!recordedLines.isEmpty()) {
322 try {
323 ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>();
324 result.putAll(loadProvidedChecksums(summaryFile));
325 result.putAll(recordedLines);
326
327 LOGGER.info("Saving {} checksums to '{}'", result.size(), summaryFile);
328 FileUtils.writeFileWithBackup(
329 summaryFile,
330 p -> Files.write(
331 p,
332 result.entrySet().stream()
333 .sorted(Map.Entry.comparingByKey())
334 .map(e -> e.getValue() + " " + e.getKey())
335 .collect(toList())));
336 } catch (IOException e) {
337 exceptions.add(e);
338 }
339 }
340 }
341 MultiRuntimeException.mayThrow("session save checksums failure", exceptions);
342 }
343 }