001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.internal.impl.checksum; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023import javax.inject.Singleton; 024 025import java.io.BufferedReader; 026import java.io.IOException; 027import java.io.UncheckedIOException; 028import java.nio.charset.StandardCharsets; 029import java.nio.file.Files; 030import java.nio.file.Path; 031import java.util.ArrayList; 032import java.util.HashMap; 033import java.util.List; 034import java.util.Map; 035import java.util.Objects; 036import java.util.concurrent.ConcurrentHashMap; 037import java.util.concurrent.atomic.AtomicBoolean; 038import java.util.function.Function; 039import java.util.stream.Collectors; 040 041import org.eclipse.aether.MultiRuntimeException; 042import org.eclipse.aether.RepositorySystemSession; 043import org.eclipse.aether.artifact.Artifact; 044import org.eclipse.aether.impl.RepositorySystemLifecycle; 045import org.eclipse.aether.internal.impl.LocalPathComposer; 046import org.eclipse.aether.repository.ArtifactRepository; 047import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory; 048import org.eclipse.aether.spi.io.PathProcessor; 049import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory; 050import org.eclipse.aether.util.ConfigUtils; 051import org.slf4j.Logger; 052import org.slf4j.LoggerFactory; 053 054import static java.util.Objects.requireNonNull; 055 056/** 057 * Compact file {@link FileTrustedChecksumsSourceSupport} implementation that use specified directory as base 058 * directory, where it expects a "summary" file named as "checksums.${checksumExt}" for each checksum algorithm. 059 * File format is GNU Coreutils compatible: each line holds checksum followed by two spaces and artifact relative path 060 * (from local repository root, without leading "./"). This means that trusted checksums summary file can be used to 061 * validate artifacts or generate it using standard GNU tools like GNU {@code sha1sum} is (for BSD derivatives same 062 * file can be used with {@code -r} switch). 063 * <p> 064 * The format supports comments "#" (hash) and empty lines for easier structuring the file content, and both are 065 * ignored. Also, their presence makes the summary file incompatible with GNU Coreutils format. On save of the 066 * summary file, the comments and empty lines are lost, and file is sorted by path names for easier diffing 067 * (2nd column in file). 068 * <p> 069 * The source by default is "origin aware", and it will factor in origin repository ID as well into summary file name, 070 * for example "checksums-central.sha256". 071 * <p> 072 * Example commands for managing summary file (in examples will use repository ID "central"): 073 * <ul> 074 * <li>To create summary file: {@code find * -not -name "checksums-central.sha256" -type f -print0 | 075 * xargs -0 sha256sum | sort -k 2 > checksums-central.sha256}</li> 076 * <li>To verify artifacts using summary file: {@code sha256sum --quiet -c checksums-central.sha256}</li> 077 * </ul> 078 * <p> 079 * The checksums summary file is lazily loaded and remains cached during lifetime of the component, so file changes 080 * during lifecycle of the component are not picked up. This implementation can be simultaneously used to lookup and 081 * also record checksums. The recorded checksums will become visible for every session, and will be flushed 082 * at repository system shutdown, merged with existing ones on disk. 083 * <p> 084 * The name of this implementation is "summaryFile". 085 * 086 * @see <a href="https://man7.org/linux/man-pages/man1/sha1sum.1.html">sha1sum man page</a> 087 * @see <a href="https://www.gnu.org/software/coreutils/manual/coreutils.html#md5sum-invocation">GNU Coreutils: md5sum</a> 088 * @since 1.9.0 089 */ 090@Singleton 091@Named(SummaryFileTrustedChecksumsSource.NAME) 092public final class SummaryFileTrustedChecksumsSource extends FileTrustedChecksumsSourceSupport { 093 public static final String NAME = "summaryFile"; 094 095 private static final String CONFIG_PROPS_PREFIX = 096 FileTrustedChecksumsSourceSupport.CONFIG_PROPS_PREFIX + NAME + "."; 097 098 /** 099 * Is checksum source enabled? 100 * 101 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 102 * @configurationType {@link java.lang.Boolean} 103 * @configurationDefaultValue false 104 */ 105 public static final String CONFIG_PROP_ENABLED = FileTrustedChecksumsSourceSupport.CONFIG_PROPS_PREFIX + NAME; 106 107 /** 108 * The basedir where checksums are. If relative, is resolved from local repository root. 109 * 110 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 111 * @configurationType {@link java.lang.String} 112 * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR} 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 * Is source origin aware? 120 * 121 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 122 * @configurationType {@link java.lang.Boolean} 123 * @configurationDefaultValue true 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 RepositoryKeyFunctionFactory repoKeyFunctionFactory, 146 LocalPathComposer localPathComposer, 147 RepositorySystemLifecycle repositorySystemLifecycle, 148 PathProcessor pathProcessor) { 149 super(repoKeyFunctionFactory); 150 this.localPathComposer = requireNonNull(localPathComposer); 151 this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle); 152 this.pathProcessor = requireNonNull(pathProcessor); 153 this.checksums = new ConcurrentHashMap<>(); 154 this.changedChecksums = new ConcurrentHashMap<>(); 155 this.onShutdownHandlerRegistered = new AtomicBoolean(false); 156 } 157 158 @Override 159 protected boolean isEnabled(RepositorySystemSession session) { 160 return ConfigUtils.getBoolean(session, false, CONFIG_PROP_ENABLED); 161 } 162 163 private boolean isOriginAware(RepositorySystemSession session) { 164 return ConfigUtils.getBoolean(session, true, CONFIG_PROP_ORIGIN_AWARE); 165 } 166 167 @Override 168 protected Map<String, String> doGetTrustedArtifactChecksums( 169 RepositorySystemSession session, 170 Artifact artifact, 171 ArtifactRepository artifactRepository, 172 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) { 173 final HashMap<String, String> result = new HashMap<>(); 174 final Path basedir = getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false); 175 if (Files.isDirectory(basedir)) { 176 final String artifactPath = localPathComposer.getPathForArtifact(artifact, false); 177 final boolean originAware = isOriginAware(session); 178 for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) { 179 Path summaryFile = summaryFile( 180 basedir, 181 originAware, 182 repositoryKey(session, artifactRepository), 183 checksumAlgorithmFactory.getFileExtension()); 184 ConcurrentHashMap<String, String> algorithmChecksums = 185 checksums.computeIfAbsent(summaryFile, f -> loadProvidedChecksums(summaryFile)); 186 String checksum = algorithmChecksums.get(artifactPath); 187 if (checksum != null) { 188 result.put(checksumAlgorithmFactory.getName(), checksum); 189 } 190 } 191 } 192 return result; 193 } 194 195 @Override 196 protected Writer doGetTrustedArtifactChecksumsWriter(RepositorySystemSession session) { 197 if (onShutdownHandlerRegistered.compareAndSet(false, true)) { 198 repositorySystemLifecycle.addOnSystemEndedHandler(this::saveRecordedLines); 199 } 200 return new SummaryFileWriter( 201 checksums, 202 getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, true), 203 isOriginAware(session), 204 r -> repositoryKey(session, r)); 205 } 206 207 /** 208 * Returns the summary file path. The file itself and its parent directories may not exist, this method merely 209 * calculate the path. 210 */ 211 private Path summaryFile(Path basedir, boolean originAware, String safeRepositoryId, String checksumExtension) { 212 String fileName = CHECKSUMS_FILE_PREFIX; 213 if (originAware) { 214 fileName += "-" + safeRepositoryId; 215 } 216 return basedir.resolve(fileName + "." + checksumExtension); 217 } 218 219 private ConcurrentHashMap<String, String> loadProvidedChecksums(Path summaryFile) { 220 ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>(); 221 if (Files.isRegularFile(summaryFile)) { 222 try (BufferedReader reader = Files.newBufferedReader(summaryFile, StandardCharsets.UTF_8)) { 223 String line; 224 while ((line = reader.readLine()) != null) { 225 if (!line.startsWith("#") && !line.isEmpty()) { 226 String[] parts = line.split(" ", 2); 227 if (parts.length == 2) { 228 String newChecksum = parts[0]; 229 String artifactPath = parts[1]; 230 String oldChecksum = result.put(artifactPath, newChecksum); 231 if (oldChecksum != null) { 232 if (Objects.equals(oldChecksum, newChecksum)) { 233 LOGGER.warn( 234 "Checksums file '{}' contains duplicate checksums for artifact {}: {}", 235 summaryFile, 236 artifactPath, 237 oldChecksum); 238 } else { 239 LOGGER.warn( 240 "Checksums file '{}' contains different checksums for artifact {}: " 241 + "old '{}' replaced by new '{}'", 242 summaryFile, 243 artifactPath, 244 oldChecksum, 245 newChecksum); 246 } 247 } 248 } else { 249 LOGGER.warn("Checksums file '{}' ignored malformed line '{}'", summaryFile, line); 250 } 251 } 252 } 253 } catch (IOException e) { 254 throw new UncheckedIOException(e); 255 } 256 LOGGER.info("Loaded {} trusted checksums from {}", result.size(), summaryFile); 257 } 258 return result; 259 } 260 261 private class SummaryFileWriter implements Writer { 262 private final ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache; 263 264 private final Path basedir; 265 266 private final boolean originAware; 267 268 private final Function<ArtifactRepository, String> repositoryKeyFunction; 269 270 private SummaryFileWriter( 271 ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache, 272 Path basedir, 273 boolean originAware, 274 Function<ArtifactRepository, String> repositoryKeyFunction) { 275 this.cache = cache; 276 this.basedir = basedir; 277 this.originAware = originAware; 278 this.repositoryKeyFunction = repositoryKeyFunction; 279 } 280 281 @Override 282 public void addTrustedArtifactChecksums( 283 Artifact artifact, 284 ArtifactRepository artifactRepository, 285 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories, 286 Map<String, String> trustedArtifactChecksums) { 287 String artifactPath = localPathComposer.getPathForArtifact(artifact, false); 288 for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) { 289 Path summaryFile = summaryFile( 290 basedir, 291 originAware, 292 repositoryKeyFunction.apply(artifactRepository), 293 checksumAlgorithmFactory.getFileExtension()); 294 String checksum = requireNonNull(trustedArtifactChecksums.get(checksumAlgorithmFactory.getName())); 295 296 String oldChecksum = cache.computeIfAbsent(summaryFile, k -> loadProvidedChecksums(summaryFile)) 297 .put(artifactPath, checksum); 298 299 if (oldChecksum == null) { 300 changedChecksums.put(summaryFile, Boolean.TRUE); // new 301 } else if (!Objects.equals(oldChecksum, checksum)) { 302 changedChecksums.put(summaryFile, Boolean.TRUE); // replaced 303 LOGGER.info( 304 "Trusted checksum for artifact {} replaced: old {}, new {}", 305 artifact, 306 oldChecksum, 307 checksum); 308 } 309 } 310 } 311 } 312 313 /** 314 * On-close handler that saves recorded checksums, if any. 315 */ 316 private void saveRecordedLines() { 317 if (changedChecksums.isEmpty()) { 318 return; 319 } 320 321 ArrayList<Exception> exceptions = new ArrayList<>(); 322 for (Map.Entry<Path, ConcurrentHashMap<String, String>> entry : checksums.entrySet()) { 323 Path summaryFile = entry.getKey(); 324 if (changedChecksums.get(summaryFile) != Boolean.TRUE) { 325 continue; 326 } 327 ConcurrentHashMap<String, String> recordedLines = entry.getValue(); 328 if (!recordedLines.isEmpty()) { 329 try { 330 ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>(); 331 result.putAll(loadProvidedChecksums(summaryFile)); 332 result.putAll(recordedLines); 333 334 LOGGER.info("Saving {} checksums to '{}'", result.size(), summaryFile); 335 pathProcessor.writeWithBackup( 336 summaryFile, 337 result.entrySet().stream() 338 .sorted(Map.Entry.comparingByKey()) 339 .map(e -> e.getValue() + " " + e.getKey()) 340 .collect(Collectors.joining(System.lineSeparator()))); 341 } catch (IOException e) { 342 exceptions.add(e); 343 } 344 } 345 } 346 MultiRuntimeException.mayThrow("session save checksums failure", exceptions); 347 } 348}