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 = checksums.computeIfAbsent(summaryFile, f -> {
132 ConcurrentHashMap<String, String> loaded = loadProvidedChecksums(summaryFile);
133 if (Files.isRegularFile(summaryFile)) {
134 LOGGER.info(
135 "Loaded {} {} trusted checksums for remote repository {}",
136 loaded.size(),
137 checksumAlgorithmFactory.getName(),
138 artifactRepository.getId());
139 }
140 return loaded;
141 });
142 String checksum = algorithmChecksums.get(artifactPath);
143 if (checksum != null) {
144 result.put(checksumAlgorithmFactory.getName(), checksum);
145 }
146 }
147 }
148 return result;
149 }
150
151 @Override
152 protected SummaryFileWriter doGetTrustedArtifactChecksumsWriter(RepositorySystemSession session) {
153 if (onShutdownHandlerRegistered.compareAndSet(false, true)) {
154 repositorySystemLifecycle.addOnSystemEndedHandler(this::saveRecordedLines);
155 }
156 return new SummaryFileWriter(checksums, getBasedir(session, true), isOriginAware(session));
157 }
158
159
160
161
162
163 private Path summaryFile(Path basedir, boolean originAware, String repositoryId, String checksumExtension) {
164 String fileName = CHECKSUMS_FILE_PREFIX;
165 if (originAware) {
166 fileName += "-" + repositoryId;
167 }
168 return basedir.resolve(fileName + "." + checksumExtension);
169 }
170
171 private ConcurrentHashMap<String, String> loadProvidedChecksums(Path summaryFile) {
172 ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>();
173 if (Files.isRegularFile(summaryFile)) {
174 try (BufferedReader reader = Files.newBufferedReader(summaryFile, StandardCharsets.UTF_8)) {
175 String line;
176 while ((line = reader.readLine()) != null) {
177 if (!line.startsWith("#") && !line.isEmpty()) {
178 String[] parts = line.split(" ", 2);
179 if (parts.length == 2) {
180 String newChecksum = parts[0];
181 String artifactPath = parts[1];
182 String oldChecksum = result.put(artifactPath, newChecksum);
183 if (oldChecksum != null) {
184 if (Objects.equals(oldChecksum, newChecksum)) {
185 LOGGER.warn(
186 "Checksums file '{}' contains duplicate checksums for artifact {}: {}",
187 summaryFile,
188 artifactPath,
189 oldChecksum);
190 } else {
191 LOGGER.warn(
192 "Checksums file '{}' contains different checksums for artifact {}: "
193 + "old '{}' replaced by new '{}'",
194 summaryFile,
195 artifactPath,
196 oldChecksum,
197 newChecksum);
198 }
199 }
200 } else {
201 LOGGER.warn("Checksums file '{}' ignored malformed line '{}'", summaryFile, line);
202 }
203 }
204 }
205 } catch (IOException e) {
206 throw new UncheckedIOException(e);
207 }
208 }
209 return result;
210 }
211
212 private class SummaryFileWriter implements Writer {
213 private final ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache;
214
215 private final Path basedir;
216
217 private final boolean originAware;
218
219 private SummaryFileWriter(
220 ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache, Path basedir, boolean originAware) {
221 this.cache = cache;
222 this.basedir = basedir;
223 this.originAware = originAware;
224 }
225
226 @Override
227 public void addTrustedArtifactChecksums(
228 Artifact artifact,
229 ArtifactRepository artifactRepository,
230 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
231 Map<String, String> trustedArtifactChecksums) {
232 String artifactPath = localPathComposer.getPathForArtifact(artifact, false);
233 for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
234 Path summaryFile = summaryFile(
235 basedir, originAware, artifactRepository.getId(), checksumAlgorithmFactory.getFileExtension());
236 String checksum = requireNonNull(trustedArtifactChecksums.get(checksumAlgorithmFactory.getName()));
237
238 String oldChecksum = cache.computeIfAbsent(summaryFile, k -> loadProvidedChecksums(summaryFile))
239 .put(artifactPath, checksum);
240
241 if (oldChecksum == null) {
242 changedChecksums.put(summaryFile, Boolean.TRUE);
243 } else if (!Objects.equals(oldChecksum, checksum)) {
244 changedChecksums.put(summaryFile, Boolean.TRUE);
245 LOGGER.info(
246 "Trusted checksum for artifact {} replaced: old {}, new {}",
247 artifact,
248 oldChecksum,
249 checksum);
250 }
251 }
252 }
253 }
254
255
256
257
258 private void saveRecordedLines() {
259 if (changedChecksums.isEmpty()) {
260 return;
261 }
262
263 ArrayList<Exception> exceptions = new ArrayList<>();
264 for (Map.Entry<Path, ConcurrentHashMap<String, String>> entry : checksums.entrySet()) {
265 Path summaryFile = entry.getKey();
266 if (changedChecksums.get(summaryFile) != Boolean.TRUE) {
267 continue;
268 }
269 ConcurrentHashMap<String, String> recordedLines = entry.getValue();
270 if (!recordedLines.isEmpty()) {
271 try {
272 ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>();
273 result.putAll(loadProvidedChecksums(summaryFile));
274 result.putAll(recordedLines);
275
276 LOGGER.info("Saving {} checksums to '{}'", result.size(), summaryFile);
277 FileUtils.writeFileWithBackup(
278 summaryFile,
279 p -> Files.write(
280 p,
281 result.entrySet().stream()
282 .sorted(Map.Entry.comparingByValue())
283 .map(e -> e.getValue() + " " + e.getKey())
284 .collect(toList())));
285 } catch (IOException e) {
286 exceptions.add(e);
287 }
288 }
289 }
290 MultiRuntimeException.mayThrow("session save checksums failure", exceptions);
291 }
292 }