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
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
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 @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
152
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);
235 } else if (!Objects.equals(oldChecksum, checksum)) {
236 changedChecksums.put(summaryFile, Boolean.TRUE);
237 LOGGER.info(
238 "Trusted checksum for artifact {} replaced: old {}, new {}",
239 artifact,
240 oldChecksum,
241 checksum);
242 }
243 }
244 }
245 }
246
247
248
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 }