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; 038 039import org.eclipse.aether.MultiRuntimeException; 040import org.eclipse.aether.RepositorySystemSession; 041import org.eclipse.aether.artifact.Artifact; 042import org.eclipse.aether.impl.RepositorySystemLifecycle; 043import org.eclipse.aether.internal.impl.LocalPathComposer; 044import org.eclipse.aether.repository.ArtifactRepository; 045import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory; 046import org.eclipse.aether.util.ConfigUtils; 047import org.eclipse.aether.util.FileUtils; 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050 051import static java.util.Objects.requireNonNull; 052import static java.util.stream.Collectors.toList; 053 054/** 055 * Compact file {@link FileTrustedChecksumsSourceSupport} implementation that use specified directory as base 056 * directory, where it expects a "summary" file named as "checksums.${checksumExt}" for each checksum algorithm. 057 * File format is GNU Coreutils compatible: each line holds checksum followed by two spaces and artifact relative path 058 * (from local repository root, without leading "./"). This means that trusted checksums summary file can be used to 059 * validate artifacts or generate it using standard GNU tools like GNU {@code sha1sum} is (for BSD derivatives same 060 * file can be used with {@code -r} switch). 061 * <p> 062 * The format supports comments "#" (hash) and empty lines for easier structuring the file content, and both are 063 * ignored. Also, their presence makes the summary file incompatible with GNU Coreutils format. On save of the 064 * summary file, the comments and empty lines are lost, and file is sorted by path names for easier diffing 065 * (2nd column in file). 066 * <p> 067 * The source by default is "origin aware", and it will factor in origin repository ID as well into summary file name, 068 * for example "checksums-central.sha256". 069 * <p> 070 * Example commands for managing summary file (in examples will use repository ID "central"): 071 * <ul> 072 * <li>To create summary file: {@code find * -not -name "checksums-central.sha256" -type f -print0 | 073 * xargs -0 sha256sum | sort -k 2 > checksums-central.sha256}</li> 074 * <li>To verify artifacts using summary file: {@code sha256sum --quiet -c checksums-central.sha256}</li> 075 * </ul> 076 * <p> 077 * The checksums summary file is lazily loaded and remains cached during lifetime of the component, so file changes 078 * during lifecycle of the component are not picked up. This implementation can be simultaneously used to lookup and 079 * also record checksums. The recorded checksums will become visible for every session, and will be flushed 080 * at repository system shutdown, merged with existing ones on disk. 081 * <p> 082 * The name of this implementation is "summaryFile". 083 * 084 * @see <a href="https://man7.org/linux/man-pages/man1/sha1sum.1.html">sha1sum man page</a> 085 * @see <a href="https://www.gnu.org/software/coreutils/manual/coreutils.html#md5sum-invocation">GNU Coreutils: md5sum</a> 086 * @since 1.9.0 087 */ 088@Singleton 089@Named(SummaryFileTrustedChecksumsSource.NAME) 090public final class SummaryFileTrustedChecksumsSource extends FileTrustedChecksumsSourceSupport { 091 public static final String NAME = "summaryFile"; 092 093 private static final String CONFIG_PROPS_PREFIX = 094 FileTrustedChecksumsSourceSupport.CONFIG_PROPS_PREFIX + NAME + "."; 095 096 /** 097 * Is checksum source enabled? 098 * 099 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 100 * @configurationType {@link java.lang.Boolean} 101 * @configurationDefaultValue false 102 */ 103 public static final String CONFIG_PROP_ENABLED = FileTrustedChecksumsSourceSupport.CONFIG_PROPS_PREFIX + NAME; 104 105 /** 106 * The basedir where checksums are. If relative, is resolved from local repository root. 107 * 108 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 109 * @configurationType {@link java.lang.String} 110 * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR} 111 */ 112 public static final String CONFIG_PROP_BASEDIR = CONFIG_PROPS_PREFIX + "basedir"; 113 114 public static final String LOCAL_REPO_PREFIX_DIR = ".checksums"; 115 116 /** 117 * Is source origin aware? 118 * 119 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 120 * @configurationType {@link java.lang.Boolean} 121 * @configurationDefaultValue true 122 */ 123 public static final String CONFIG_PROP_ORIGIN_AWARE = CONFIG_PROPS_PREFIX + "originAware"; 124 125 public static final String CHECKSUMS_FILE_PREFIX = "checksums"; 126 127 private static final Logger LOGGER = LoggerFactory.getLogger(SummaryFileTrustedChecksumsSource.class); 128 129 private final LocalPathComposer localPathComposer; 130 131 private final RepositorySystemLifecycle repositorySystemLifecycle; 132 133 private final ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> checksums; 134 135 private final ConcurrentHashMap<Path, Boolean> changedChecksums; 136 137 private final AtomicBoolean onShutdownHandlerRegistered; 138 139 @Inject 140 public SummaryFileTrustedChecksumsSource( 141 LocalPathComposer localPathComposer, RepositorySystemLifecycle repositorySystemLifecycle) { 142 this.localPathComposer = requireNonNull(localPathComposer); 143 this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle); 144 this.checksums = new ConcurrentHashMap<>(); 145 this.changedChecksums = new ConcurrentHashMap<>(); 146 this.onShutdownHandlerRegistered = new AtomicBoolean(false); 147 } 148 149 @Override 150 protected boolean isEnabled(RepositorySystemSession session) { 151 return ConfigUtils.getBoolean(session, false, CONFIG_PROP_ENABLED); 152 } 153 154 private boolean isOriginAware(RepositorySystemSession session) { 155 return ConfigUtils.getBoolean(session, true, CONFIG_PROP_ORIGIN_AWARE); 156 } 157 158 @Override 159 protected Map<String, String> doGetTrustedArtifactChecksums( 160 RepositorySystemSession session, 161 Artifact artifact, 162 ArtifactRepository artifactRepository, 163 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) { 164 final HashMap<String, String> result = new HashMap<>(); 165 final Path basedir = getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false); 166 if (Files.isDirectory(basedir)) { 167 final String artifactPath = localPathComposer.getPathForArtifact(artifact, false); 168 final boolean originAware = isOriginAware(session); 169 for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) { 170 Path summaryFile = summaryFile( 171 basedir, originAware, artifactRepository.getId(), checksumAlgorithmFactory.getFileExtension()); 172 ConcurrentHashMap<String, String> algorithmChecksums = 173 checksums.computeIfAbsent(summaryFile, f -> loadProvidedChecksums(summaryFile)); 174 String checksum = algorithmChecksums.get(artifactPath); 175 if (checksum != null) { 176 result.put(checksumAlgorithmFactory.getName(), checksum); 177 } 178 } 179 } 180 return result; 181 } 182 183 @Override 184 protected Writer doGetTrustedArtifactChecksumsWriter(RepositorySystemSession session) { 185 if (onShutdownHandlerRegistered.compareAndSet(false, true)) { 186 repositorySystemLifecycle.addOnSystemEndedHandler(this::saveRecordedLines); 187 } 188 return new SummaryFileWriter( 189 checksums, 190 getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, true), 191 isOriginAware(session)); 192 } 193 194 /** 195 * Returns the summary file path. The file itself and its parent directories may not exist, this method merely 196 * calculate the path. 197 */ 198 private Path summaryFile(Path basedir, boolean originAware, String repositoryId, String checksumExtension) { 199 String fileName = CHECKSUMS_FILE_PREFIX; 200 if (originAware) { 201 fileName += "-" + repositoryId; 202 } 203 return basedir.resolve(fileName + "." + checksumExtension); 204 } 205 206 private ConcurrentHashMap<String, String> loadProvidedChecksums(Path summaryFile) { 207 ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>(); 208 if (Files.isRegularFile(summaryFile)) { 209 try (BufferedReader reader = Files.newBufferedReader(summaryFile, StandardCharsets.UTF_8)) { 210 String line; 211 while ((line = reader.readLine()) != null) { 212 if (!line.startsWith("#") && !line.isEmpty()) { 213 String[] parts = line.split(" ", 2); 214 if (parts.length == 2) { 215 String newChecksum = parts[0]; 216 String artifactPath = parts[1]; 217 String oldChecksum = result.put(artifactPath, newChecksum); 218 if (oldChecksum != null) { 219 if (Objects.equals(oldChecksum, newChecksum)) { 220 LOGGER.warn( 221 "Checksums file '{}' contains duplicate checksums for artifact {}: {}", 222 summaryFile, 223 artifactPath, 224 oldChecksum); 225 } else { 226 LOGGER.warn( 227 "Checksums file '{}' contains different checksums for artifact {}: " 228 + "old '{}' replaced by new '{}'", 229 summaryFile, 230 artifactPath, 231 oldChecksum, 232 newChecksum); 233 } 234 } 235 } else { 236 LOGGER.warn("Checksums file '{}' ignored malformed line '{}'", summaryFile, line); 237 } 238 } 239 } 240 } catch (IOException e) { 241 throw new UncheckedIOException(e); 242 } 243 LOGGER.info("Loaded {} trusted checksums from {}", result.size(), summaryFile); 244 } 245 return result; 246 } 247 248 private class SummaryFileWriter implements Writer { 249 private final ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache; 250 251 private final Path basedir; 252 253 private final boolean originAware; 254 255 private SummaryFileWriter( 256 ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> cache, Path basedir, boolean originAware) { 257 this.cache = cache; 258 this.basedir = basedir; 259 this.originAware = originAware; 260 } 261 262 @Override 263 public void addTrustedArtifactChecksums( 264 Artifact artifact, 265 ArtifactRepository artifactRepository, 266 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories, 267 Map<String, String> trustedArtifactChecksums) { 268 String artifactPath = localPathComposer.getPathForArtifact(artifact, false); 269 for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) { 270 Path summaryFile = summaryFile( 271 basedir, originAware, artifactRepository.getId(), checksumAlgorithmFactory.getFileExtension()); 272 String checksum = requireNonNull(trustedArtifactChecksums.get(checksumAlgorithmFactory.getName())); 273 274 String oldChecksum = cache.computeIfAbsent(summaryFile, k -> loadProvidedChecksums(summaryFile)) 275 .put(artifactPath, checksum); 276 277 if (oldChecksum == null) { 278 changedChecksums.put(summaryFile, Boolean.TRUE); // new 279 } else if (!Objects.equals(oldChecksum, checksum)) { 280 changedChecksums.put(summaryFile, Boolean.TRUE); // replaced 281 LOGGER.info( 282 "Trusted checksum for artifact {} replaced: old {}, new {}", 283 artifact, 284 oldChecksum, 285 checksum); 286 } 287 } 288 } 289 } 290 291 /** 292 * On-close handler that saves recorded checksums, if any. 293 */ 294 private void saveRecordedLines() { 295 if (changedChecksums.isEmpty()) { 296 return; 297 } 298 299 ArrayList<Exception> exceptions = new ArrayList<>(); 300 for (Map.Entry<Path, ConcurrentHashMap<String, String>> entry : checksums.entrySet()) { 301 Path summaryFile = entry.getKey(); 302 if (changedChecksums.get(summaryFile) != Boolean.TRUE) { 303 continue; 304 } 305 ConcurrentHashMap<String, String> recordedLines = entry.getValue(); 306 if (!recordedLines.isEmpty()) { 307 try { 308 ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>(); 309 result.putAll(loadProvidedChecksums(summaryFile)); 310 result.putAll(recordedLines); 311 312 LOGGER.info("Saving {} checksums to '{}'", result.size(), summaryFile); 313 FileUtils.writeFileWithBackup( 314 summaryFile, 315 p -> Files.write( 316 p, 317 result.entrySet().stream() 318 .sorted(Map.Entry.comparingByKey()) 319 .map(e -> e.getValue() + " " + e.getKey()) 320 .collect(toList()))); 321 } catch (IOException e) { 322 exceptions.add(e); 323 } 324 } 325 } 326 MultiRuntimeException.mayThrow("session save checksums failure", exceptions); 327 } 328}