This is an automated email from the ASF dual-hosted git repository.
cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to refs/heads/master by this push:
new 6e74e9ee [MRESOLVER-276][MRESOLVER-268] Post artifact resolve hook and
resolver checksum verification (#200)
6e74e9ee is described below
commit 6e74e9ee279f827140553846b6421fe9b75d3ad9
Author: Tamas Cservenak <[email protected]>
AuthorDate: Tue Oct 11 08:52:05 2022 +0200
[MRESOLVER-276][MRESOLVER-268] Post artifact resolve hook and resolver
checksum verification (#200)
Introduce "post artifact resolve hook" feature: is able to post process
resolved artifacts just before they are returned to caller and affect
resolution outcome by signaling failure (by augmenting results).
Adds one implementation as well.
High level changes:
* introduced `trusted-checksum` post processor: uses
`TrustedChecksumsSource`
to validate ALL resolved artifacts against available "trusted"
checksums source. Added configuration options like "checksumAlgorithm"
(def: sha1), "failIfMissing" (def: false) and "record" (def: false).
* extended `TrustedChecksumsSource` to be able to write checksums as well
(when post-processor "records")
When the `trusted-checksum` post processor is enabled, it will use any
enabled `TrustedChecksumSource` to get checksums, and match the calculated
checksum with provided ones. If no enabled source, nothing happens (as no
source to compare checksums with). By default missing checksum does not
fail, only mismatches. One can turn on `failIfMissing` to achieve this
stricter behaviour and enforce checksums on all post-processed artifacts.
Finally, as possibility (for pre-populating checksusm or just testing),
the `record` option is added, that post-processes all resolved artifacts
by calculating their checksums, and stores them in any enabled trusted
checksum source (both file backed implementations are extended to
support this). Again, if no trusted checksum source enabled (or none of
enabled ones are writable), record operation will result in "no-op".
In current PR the summary-file trusted checksum source will produce
overlapping (many many double checksums) files, as there is no logic
added to enforce "uniqueness" of summary file.
---
https://issues.apache.org/jira/browse/MRESOLVER-276
https://issues.apache.org/jira/browse/MRESOLVER-268
---
.../eclipse/aether/impl/guice/AetherModule.java | 17 ++
.../internal/impl/DefaultArtifactResolver.java | 28 ++-
.../impl/Maven2RepositoryLayoutFactory.java | 16 +-
.../DefaultChecksumAlgorithmFactorySelector.java | 6 +-
.../FileTrustedChecksumsSourceSupport.java | 49 ++++-
.../SparseDirectoryTrustedChecksumsSource.java | 79 +++++--
.../SummaryFileTrustedChecksumsSource.java | 121 +++++++++--
.../ArtifactResolverPostProcessorSupport.java | 64 ++++++
...stedChecksumsArtifactResolverPostProcessor.java | 231 +++++++++++++++++++++
.../internal/impl/DefaultArtifactResolverTest.java | 2 +
.../FileTrustedChecksumsSourceTestSupport.java | 20 +-
.../SparseDirectoryTrustedChecksumsSourceTest.java | 8 +-
.../SummaryFileTrustedChecksumsSourceTest.java | 8 +-
...ChecksumsArtifactResolverPostProcessorTest.java | 226 ++++++++++++++++++++
.../spi/checksums/TrustedChecksumsSource.java | 32 ++-
.../checksum/ChecksumAlgorithmFactorySelector.java | 16 ++
.../checksum/ProvidedChecksumsSource.java | 12 +-
.../resolution/ArtifactResolverPostProcessor.java | 47 +++++
.../java/org/eclipse/aether/util/ConfigUtils.java | 30 +++
19 files changed, 940 insertions(+), 72 deletions(-)
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
index 2e927dea..e35c2e30 100644
---
a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
@@ -57,6 +57,7 @@ import
org.eclipse.aether.internal.impl.checksum.TrustedToProvidedChecksumsSourc
import org.eclipse.aether.internal.impl.collect.DependencyCollectorDelegate;
import org.eclipse.aether.internal.impl.collect.bf.BfDependencyCollector;
import org.eclipse.aether.internal.impl.collect.df.DfDependencyCollector;
+import
org.eclipse.aether.internal.impl.resolution.TrustedChecksumsArtifactResolverPostProcessor;
import org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory;
import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
import
org.eclipse.aether.internal.impl.synccontext.named.providers.DiscriminatingNameMapperProvider;
@@ -103,6 +104,7 @@ import
org.eclipse.aether.spi.connector.transport.TransporterProvider;
import org.eclipse.aether.spi.io.FileProcessor;
import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
import org.eclipse.aether.spi.synccontext.SyncContextFactory;
import org.slf4j.ILoggerFactory;
@@ -200,6 +202,10 @@ public class AetherModule
bind( TrustedChecksumsSource.class ).annotatedWith( Names.named(
SummaryFileTrustedChecksumsSource.NAME ) )
.to( SummaryFileTrustedChecksumsSource.class ).in(
Singleton.class );
+ bind( ArtifactResolverPostProcessor.class )
+ .annotatedWith( Names.named(
TrustedChecksumsArtifactResolverPostProcessor.NAME ) )
+ .to( TrustedChecksumsArtifactResolverPostProcessor.class ).in(
Singleton.class );
+
bind( ChecksumAlgorithmFactory.class ).annotatedWith( Names.named(
Md5ChecksumAlgorithmFactory.NAME ) )
.to( Md5ChecksumAlgorithmFactory.class );
bind( ChecksumAlgorithmFactory.class ).annotatedWith( Names.named(
Sha1ChecksumAlgorithmFactory.NAME ) )
@@ -240,6 +246,17 @@ public class AetherModule
}
+ @Provides
+ @Singleton
+ Map<String, ArtifactResolverPostProcessor> artifactResolverProcessors(
+ @Named( TrustedChecksumsArtifactResolverPostProcessor.NAME )
ArtifactResolverPostProcessor trustedChecksums
+ )
+ {
+ Map<String, ArtifactResolverPostProcessor> result = new HashMap<>();
+ result.put( TrustedChecksumsArtifactResolverPostProcessor.NAME,
trustedChecksums );
+ return Collections.unmodifiableMap( result );
+ }
+
@Provides
@Singleton
Map<String, DependencyCollectorDelegate> dependencyCollectorDelegates(
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
index 73400033..ac45cfee 100644
---
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
@@ -30,6 +30,9 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import static java.util.Objects.requireNonNull;
+
+import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.aether.RepositoryEvent;
@@ -44,6 +47,8 @@ import org.eclipse.aether.impl.OfflineController;
import org.eclipse.aether.impl.RemoteRepositoryManager;
import org.eclipse.aether.impl.RepositoryConnectorProvider;
import org.eclipse.aether.impl.RepositoryEventDispatcher;
+import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
+import org.eclipse.aether.spi.synccontext.SyncContextFactory;
import org.eclipse.aether.impl.UpdateCheck;
import org.eclipse.aether.impl.UpdateCheckManager;
import org.eclipse.aether.impl.VersionResolver;
@@ -68,7 +73,6 @@ import org.eclipse.aether.spi.connector.RepositoryConnector;
import org.eclipse.aether.spi.io.FileProcessor;
import org.eclipse.aether.spi.locator.Service;
import org.eclipse.aether.spi.locator.ServiceLocator;
-import org.eclipse.aether.spi.synccontext.SyncContextFactory;
import org.eclipse.aether.transfer.ArtifactNotFoundException;
import org.eclipse.aether.transfer.ArtifactTransferException;
import org.eclipse.aether.transfer.NoRepositoryConnectorException;
@@ -77,8 +81,6 @@ import org.eclipse.aether.util.ConfigUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static java.util.Objects.requireNonNull;
-
/**
*
*/
@@ -108,6 +110,8 @@ public class DefaultArtifactResolver
private OfflineController offlineController;
+ private Map<String, ArtifactResolverPostProcessor>
artifactResolverPostProcessors;
+
public DefaultArtifactResolver()
{
// enables default constructor
@@ -119,7 +123,8 @@ public class DefaultArtifactResolver
VersionResolver versionResolver,
UpdateCheckManager updateCheckManager,
RepositoryConnectorProvider
repositoryConnectorProvider,
RemoteRepositoryManager remoteRepositoryManager,
SyncContextFactory syncContextFactory,
- OfflineController offlineController )
+ OfflineController offlineController,
+ Map<String, ArtifactResolverPostProcessor>
artifactResolverPostProcessors )
{
setFileProcessor( fileProcessor );
setRepositoryEventDispatcher( repositoryEventDispatcher );
@@ -129,6 +134,7 @@ public class DefaultArtifactResolver
setRemoteRepositoryManager( remoteRepositoryManager );
setSyncContextFactory( syncContextFactory );
setOfflineController( offlineController );
+ setArtifactResolverPostProcessors( artifactResolverPostProcessors );
}
public void initService( ServiceLocator locator )
@@ -141,6 +147,7 @@ public class DefaultArtifactResolver
setRemoteRepositoryManager( locator.getService(
RemoteRepositoryManager.class ) );
setSyncContextFactory( locator.getService( SyncContextFactory.class )
);
setOfflineController( locator.getService( OfflineController.class ) );
+ setArtifactResolverPostProcessors( Collections.emptyMap() );
}
/**
@@ -205,6 +212,14 @@ public class DefaultArtifactResolver
return this;
}
+ public DefaultArtifactResolver setArtifactResolverPostProcessors(
+ Map<String, ArtifactResolverPostProcessor>
artifactResolverPostProcessors )
+ {
+ this.artifactResolverPostProcessors = requireNonNull(
artifactResolverPostProcessors,
+ "artifact resolver post-processors cannot be null" );
+ return this;
+ }
+
public ArtifactResult resolveArtifact( RepositorySystemSession session,
ArtifactRequest request )
throws ArtifactResolutionException
{
@@ -410,6 +425,11 @@ public class DefaultArtifactResolver
performDownloads( session, group );
}
+ for ( ArtifactResolverPostProcessor artifactResolverPostProcessor :
artifactResolverPostProcessors.values() )
+ {
+ artifactResolverPostProcessor.postProcess( session, results );
+ }
+
for ( ArtifactResult result : results )
{
ArtifactRequest request = result.getRequest();
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactory.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactory.java
index 1b76940e..6ef37964 100644
---
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactory.java
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactory.java
@@ -24,7 +24,6 @@ import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@@ -110,18 +109,11 @@ public final class Maven2RepositoryLayoutFactory
{
throw new NoRepositoryLayoutException( repository );
}
- // ensure order and uniqueness of (potentially user set) algorithm list
- LinkedHashSet<String> checksumsAlgorithmNames = Arrays.stream(
ConfigUtils.getString(
- session, DEFAULT_CHECKSUMS_ALGORITHMS,
CONFIG_PROP_CHECKSUMS_ALGORITHMS )
- .split( "," )
- ).filter( s -> s != null && !s.trim().isEmpty() ).collect(
Collectors.toCollection( LinkedHashSet::new ) );
- // validation: this loop implicitly validates the list above: selector
will throw on unknown algorithm
- List<ChecksumAlgorithmFactory> checksumsAlgorithms = new ArrayList<>(
checksumsAlgorithmNames.size() );
- for ( String checksumsAlgorithmName : checksumsAlgorithmNames )
- {
- checksumsAlgorithms.add( checksumAlgorithmFactorySelector.select(
checksumsAlgorithmName ) );
- }
+ List<ChecksumAlgorithmFactory> checksumsAlgorithms =
checksumAlgorithmFactorySelector.select(
+ ConfigUtils.parseCommaSeparatedUniqueNames(
ConfigUtils.getString(
+ session, DEFAULT_CHECKSUMS_ALGORITHMS,
CONFIG_PROP_CHECKSUMS_ALGORITHMS ) )
+ );
// ensure uniqueness of (potentially user set) extension list
Set<String> omitChecksumsForExtensions = Arrays.stream(
ConfigUtils.getString(
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/DefaultChecksumAlgorithmFactorySelector.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/DefaultChecksumAlgorithmFactorySelector.java
index d463100b..d1b2fd7c 100644
---
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/DefaultChecksumAlgorithmFactorySelector.java
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/DefaultChecksumAlgorithmFactorySelector.java
@@ -69,15 +69,15 @@ public class DefaultChecksumAlgorithmFactorySelector
public ChecksumAlgorithmFactory select( String algorithmName )
{
requireNonNull( algorithmName, "algorithmMame must not be null" );
- ChecksumAlgorithmFactory factory = factories.get( algorithmName );
+ ChecksumAlgorithmFactory factory = factories.get( algorithmName );
if ( factory == null )
{
throw new IllegalArgumentException(
String.format( "Unsupported checksum algorithm %s,
supported ones are %s",
algorithmName,
getChecksumAlgorithmFactories().stream()
- .map(
ChecksumAlgorithmFactory::getName )
- .collect( toList() )
+ .map( ChecksumAlgorithmFactory::getName )
+ .collect( toList() )
)
);
}
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
index 1f023064..1ea77638 100644
---
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
@@ -23,6 +23,7 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -93,24 +94,53 @@ abstract class FileTrustedChecksumsSourceSupport
boolean enabled = ConfigUtils.getBoolean( session, false,
CONFIG_PROP_PREFIX + this.name );
if ( enabled )
{
- Path basedir = getBasedir( session );
+ Path basedir = getBasedir( session, false );
if ( basedir != null && !checksumAlgorithmFactories.isEmpty() )
{
- Map<String, String> result = performLookup(
- session, basedir, artifact, artifactRepository,
checksumAlgorithmFactories );
-
- return result == null || result.isEmpty() ? null : result;
+ return requireNonNull(
+ performLookup( session, basedir, artifact,
artifactRepository, checksumAlgorithmFactories )
+ );
+ }
+ else
+ {
+ return Collections.emptyMap();
}
}
return null;
}
+ @Override
+ public Writer getTrustedArtifactChecksumsWriter( RepositorySystemSession
session )
+ {
+ requireNonNull( session, "session is null" );
+ boolean enabled = ConfigUtils.getBoolean( session, false,
CONFIG_PROP_PREFIX + this.name );
+ if ( enabled )
+ {
+ return getWriter( session, getBasedir( session, true ) );
+ }
+ return null;
+ }
+
+ /**
+ * Implementors MUST NOT return {@code null} at this point, as the "source
is enabled" check was already performed
+ * and IS enabled, worst can happen is checksums for asked artifact are
not available.
+ */
protected abstract Map<String, String> performLookup(
RepositorySystemSession session,
Path basedir,
Artifact artifact,
ArtifactRepository
artifactRepository,
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories );
+ /**
+ * If a subclass of this support class support
+ * {@link org.eclipse.aether.spi.checksums.TrustedChecksumsSource.Writer},
or in other words "is writable", it
+ * should override this method and return proper instance.
+ */
+ protected Writer getWriter( RepositorySystemSession session, Path basedir )
+ {
+ return null;
+ }
+
/**
* To be used by underlying implementations to form configuration property
keys properly scoped.
*/
@@ -129,15 +159,16 @@ abstract class FileTrustedChecksumsSourceSupport
}
/**
- * Uses common {@link DirectoryUtils} to calculate (but not) create
basedir for this implementation. Returns
- * {@code null} if the calculated basedir does not exist.
+ * Uses common {@link DirectoryUtils} to calculate (and maybe create)
basedir for this implementation. Returns
+ * {@code null} if the calculated basedir does not exist and {@code
mayCreate} was {@code false}. If
+ * {@code mayCreate} parameter was {@code true}, this method always
returns non-null {@link Path} or throws.
*/
- private Path getBasedir( RepositorySystemSession session )
+ private Path getBasedir( RepositorySystemSession session, boolean
mayCreate )
{
try
{
Path basedir = DirectoryUtils.resolveDirectory(
- session, LOCAL_REPO_PREFIX_DIR, configPropKey(
CONF_NAME_BASEDIR ), false );
+ session, LOCAL_REPO_PREFIX_DIR, configPropKey(
CONF_NAME_BASEDIR ), mayCreate );
if ( !Files.isDirectory( basedir ) )
{
return null;
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
index e8f88d29..1c28de08 100644
---
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
@@ -24,6 +24,7 @@ import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
@@ -82,26 +83,16 @@ public final class SparseDirectoryTrustedChecksumsSource
ArtifactRepository
artifactRepository,
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories )
{
- final String prefix;
- if ( isOriginAware( session ) )
- {
- prefix = artifactRepository.getId() + "/";
- }
- else
- {
- prefix = "";
- }
-
+ final boolean originAware = isOriginAware( session );
final HashMap<String, String> checksums = new HashMap<>();
- final String artifactPath = localPathComposer.getPathForArtifact(
artifact, false );
for ( ChecksumAlgorithmFactory checksumAlgorithmFactory :
checksumAlgorithmFactories )
{
Path checksumPath = basedir.resolve(
- prefix + artifactPath + "." +
checksumAlgorithmFactory.getFileExtension() );
+ calculateArtifactPath( originAware, artifact,
artifactRepository, checksumAlgorithmFactory ) );
if ( !Files.isRegularFile( checksumPath ) )
{
- LOGGER.debug( "Artifact '{}' checksum '{}' not found in path
'{}'",
+ LOGGER.debug( "Artifact '{}' trusted checksum '{}' not found
on path '{}'",
artifact, checksumAlgorithmFactory.getName(),
checksumPath );
continue;
}
@@ -117,9 +108,69 @@ public final class SparseDirectoryTrustedChecksumsSource
catch ( IOException e )
{
// unexpected, log, skip
- LOGGER.warn( "Could not read provided checksum for '{}' at
path '{}'", artifact, checksumPath, e );
+ LOGGER.warn( "Could not read artifact '{}' trusted checksum on
path '{}'", artifact, checksumPath, e );
}
}
return checksums;
}
+
+ @Override
+ protected SparseDirectoryWriter getWriter( RepositorySystemSession
session, Path basedir )
+ {
+ return new SparseDirectoryWriter( basedir, isOriginAware( session ) );
+ }
+
+ private String calculateArtifactPath( boolean originAware,
+ Artifact artifact,
+ ArtifactRepository
artifactRepository,
+ ChecksumAlgorithmFactory
checksumAlgorithmFactory )
+ {
+ final String prefix;
+ if ( originAware )
+ {
+ prefix = artifactRepository.getId() + "/";
+ }
+ else
+ {
+ prefix = "";
+ }
+
+ return prefix + localPathComposer.getPathForArtifact( artifact, false )
+ + "." + checksumAlgorithmFactory.getFileExtension();
+ }
+
+ private class SparseDirectoryWriter implements Writer
+ {
+ private final Path basedir;
+
+ private final boolean originAware;
+
+ private SparseDirectoryWriter( Path basedir, boolean originAware )
+ {
+ this.basedir = basedir;
+ this.originAware = originAware;
+ }
+
+ @Override
+ public void addTrustedArtifactChecksums( Artifact artifact,
ArtifactRepository artifactRepository,
+
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
+ Map<String, String>
trustedArtifactChecksums ) throws IOException
+ {
+ for ( ChecksumAlgorithmFactory checksumAlgorithmFactory :
checksumAlgorithmFactories )
+ {
+ String checksum = requireNonNull(
+ trustedArtifactChecksums.get(
checksumAlgorithmFactory.getName() ) );
+ Path checksumPath = basedir.resolve( calculateArtifactPath(
+ originAware, artifact, artifactRepository,
checksumAlgorithmFactory ) );
+ Files.createDirectories( checksumPath.getParent() );
+ Files.write( checksumPath, checksum.getBytes(
StandardCharsets.UTF_8 ) );
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ // nop
+ }
+ }
}
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
index 9379165d..d6f7e21c 100644
---
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
@@ -29,9 +29,11 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.aether.RepositorySystemSession;
@@ -42,6 +44,8 @@ import org.eclipse.aether.util.artifact.ArtifactIdUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import static java.util.Objects.requireNonNull;
+
/**
* Compact file {@link FileTrustedChecksumsSourceSupport} implementation that
use specified directory as base
* directory, where it expects a "summary" file named as
"checksums.${checksumExt}" for each checksum algorithm, and
@@ -85,16 +89,7 @@ public final class SummaryFileTrustedChecksumsSource
ArtifactRepository
artifactRepository,
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories )
{
- final String fileName;
- if ( isOriginAware( session ) )
- {
- fileName = CHECKSUMS_FILE_PREFIX + "-" +
artifactRepository.getId();
- }
- else
- {
- fileName = CHECKSUMS_FILE_PREFIX;
- }
-
+ final boolean originAware = isOriginAware( session );
final ConcurrentHashMap<String, ConcurrentHashMap<String, String>>
basedirProvidedChecksums =
(ConcurrentHashMap<String, ConcurrentHashMap<String, String>>)
session.getData()
.computeIfAbsent( CHECKSUMS_CACHE_KEY,
ConcurrentHashMap::new );
@@ -105,8 +100,7 @@ public final class SummaryFileTrustedChecksumsSource
ConcurrentHashMap<String, String> algorithmChecksums =
basedirProvidedChecksums.computeIfAbsent(
checksumAlgorithmFactory.getName(),
algName -> loadProvidedChecksums(
- basedir.resolve( fileName + "." +
checksumAlgorithmFactory.getFileExtension() )
- )
+ basedir, originAware, artifactRepository,
checksumAlgorithmFactory )
);
String checksum = algorithmChecksums.get( ArtifactIdUtils.toId(
artifact ) );
if ( checksum != null )
@@ -117,14 +111,26 @@ public final class SummaryFileTrustedChecksumsSource
return checksums;
}
- private ConcurrentHashMap<String, String> loadProvidedChecksums( Path
checksumsFile )
+ @Override
+ protected SummaryFileWriter getWriter( RepositorySystemSession session,
Path basedir )
{
+ return new SummaryFileWriter( basedir, isOriginAware( session ) );
+ }
+
+ private ConcurrentHashMap<String, String> loadProvidedChecksums( Path
basedir,
+ boolean
originAware,
+
ArtifactRepository artifactRepository,
+
ChecksumAlgorithmFactory checksumAlgorithmFactory )
+ {
+ Path checksumsFile = basedir.resolve(
+ calculateSummaryPath( originAware, artifactRepository,
checksumAlgorithmFactory ) );
ConcurrentHashMap<String, String> result = new ConcurrentHashMap<>();
if ( Files.isReadable( checksumsFile ) )
{
try ( BufferedReader reader = Files.newBufferedReader(
checksumsFile, StandardCharsets.UTF_8 ) )
{
- LOGGER.debug( "Loading provided checksums file '{}'",
checksumsFile );
+ LOGGER.debug( "Loading {} trusted checksums for remote
repository {} from '{}'",
+ checksumAlgorithmFactory.getName(),
artifactRepository.getId(), checksumsFile );
String line;
while ( ( line = reader.readLine() ) != null )
{
@@ -133,11 +139,21 @@ public final class SummaryFileTrustedChecksumsSource
String[] parts = line.split( " ", 2 );
if ( parts.length == 2 )
{
- String old = result.put( parts[0], parts[1] );
- if ( old != null )
+ String newChecksum = parts[1];
+ String oldChecksum = result.put( parts[0],
newChecksum );
+ if ( oldChecksum != null )
{
- LOGGER.warn( "Checksums file '{}' contains
duplicate checksums for artifact {}: "
- + "old '{}' replaced by new '{}'",
checksumsFile, parts[0], old, parts[1] );
+ if ( Objects.equals( oldChecksum, newChecksum
) )
+ {
+ LOGGER.warn( "Checksums file '{}' contains
duplicate checksums for artifact {}: {}",
+ checksumsFile, parts[0],
oldChecksum );
+ }
+ else
+ {
+ LOGGER.warn( "Checksums file '{}' contains
different checksums for artifact {}: "
+ + "old '{}' replaced by
new '{}'", checksumsFile, parts[0],
+ oldChecksum, newChecksum );
+ }
}
}
else
@@ -146,11 +162,14 @@ public final class SummaryFileTrustedChecksumsSource
}
}
}
+ LOGGER.info( "Loaded {} {} trusted checksums for remote
repository {}",
+ result.size(), checksumAlgorithmFactory.getName(),
artifactRepository.getId() );
}
catch ( NoSuchFileException e )
{
// strange: we tested for it above, still, we should not fail
- LOGGER.debug( "Checksums file '{}' not found", checksumsFile );
+ LOGGER.debug( "The {} trusted checksums for remote repository
{} not exist at '{}'",
+ checksumAlgorithmFactory.getName(),
artifactRepository.getId(), checksumsFile );
}
catch ( IOException e )
{
@@ -159,9 +178,71 @@ public final class SummaryFileTrustedChecksumsSource
}
else
{
- LOGGER.debug( "Checksums file '{}' not found", checksumsFile );
+ LOGGER.debug( "The {} trusted checksums for remote repository {}
not exist at '{}'",
+ checksumAlgorithmFactory.getName(),
artifactRepository.getId(), checksumsFile );
}
return result;
}
+
+ private String calculateSummaryPath( boolean originAware,
+ ArtifactRepository artifactRepository,
+ ChecksumAlgorithmFactory
checksumAlgorithmFactory )
+ {
+ final String fileName;
+ if ( originAware )
+ {
+ fileName = CHECKSUMS_FILE_PREFIX + "-" +
artifactRepository.getId();
+ }
+ else
+ {
+ fileName = CHECKSUMS_FILE_PREFIX;
+ }
+ return fileName + "." + checksumAlgorithmFactory.getFileExtension();
+ }
+
+ /**
+ * Note: this implementation will work only in single-thread (T1) model.
While not ideal, the "workaround" is
+ * possible in both, Maven and Maven Daemon: force single threaded
execution model while "recording" (in mvn:
+ * do not pass any {@code -T} CLI parameter, while for mvnd use {@code -1}
CLI parameter.
+ *
+ * TODO: this will need to be reworked for at least two reasons: a) avoid
duplicates in summary file and b)
+ * support multi threaded builds (probably will need "on session close"
hook).
+ */
+ private class SummaryFileWriter implements Writer
+ {
+ private final Path basedir;
+
+ private final boolean originAware;
+
+ private SummaryFileWriter( Path basedir, boolean originAware )
+ {
+ this.basedir = basedir;
+ this.originAware = originAware;
+ }
+
+ @Override
+ public void addTrustedArtifactChecksums( Artifact artifact,
ArtifactRepository artifactRepository,
+
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
+ Map<String, String>
trustedArtifactChecksums ) throws IOException
+ {
+ for ( ChecksumAlgorithmFactory checksumAlgorithmFactory :
checksumAlgorithmFactories )
+ {
+ String checksum = requireNonNull(
+ trustedArtifactChecksums.get(
checksumAlgorithmFactory.getName() ) );
+ String summaryLine = ArtifactIdUtils.toId( artifact ) + " " +
checksum + "\n";
+ Path summaryPath = basedir.resolve(
+ calculateSummaryPath( originAware, artifactRepository,
checksumAlgorithmFactory ) );
+ Files.createDirectories( summaryPath.getParent() );
+ Files.write( summaryPath, summaryLine.getBytes(
StandardCharsets.UTF_8 ),
+ StandardOpenOption.CREATE, StandardOpenOption.WRITE,
StandardOpenOption.APPEND );
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ // nop
+ }
+ }
}
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/ArtifactResolverPostProcessorSupport.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/ArtifactResolverPostProcessorSupport.java
new file mode 100644
index 00000000..4b09f27b
--- /dev/null
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/ArtifactResolverPostProcessorSupport.java
@@ -0,0 +1,64 @@
+package org.eclipse.aether.internal.impl.resolution;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
+import org.eclipse.aether.util.ConfigUtils;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Support class to implement {@link ArtifactResolverPostProcessor}.
+ *
+ * @since TBD
+ */
+public abstract class ArtifactResolverPostProcessorSupport
+ implements ArtifactResolverPostProcessor
+{
+ private static final String CONFIG_PROP_PREFIX =
"aether.artifactResolver.postProcessor.";
+
+ private final String name;
+
+ protected ArtifactResolverPostProcessorSupport( String name )
+ {
+ this.name = requireNonNull( name );
+ }
+
+ protected String configPropKey( String name )
+ {
+ return CONFIG_PROP_PREFIX + this.name + "." + name;
+ }
+
+ @Override
+ public void postProcess( RepositorySystemSession session,
List<ArtifactResult> artifactResults )
+ {
+ boolean enabled = ConfigUtils.getBoolean( session, false,
CONFIG_PROP_PREFIX + this.name );
+ if ( enabled )
+ {
+ doProcess( session, artifactResults );
+ }
+ }
+
+ protected abstract void doProcess( RepositorySystemSession session,
List<ArtifactResult> artifactResults );
+}
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessor.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessor.java
new file mode 100644
index 00000000..ab2b87d2
--- /dev/null
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessor.java
@@ -0,0 +1,231 @@
+package org.eclipse.aether.internal.impl.resolution;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
+import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
+import
org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactorySelector;
+import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper;
+import org.eclipse.aether.transfer.ChecksumFailureException;
+import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.artifact.ArtifactIdUtils;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Artifact resolver processor that verifies the checksums of all resolved
artifacts against trusted checksums. Is also
+ * able to "record" (calculate and write them) to trusted checksum sources,
that do support this operation.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named( TrustedChecksumsArtifactResolverPostProcessor.NAME )
+public final class TrustedChecksumsArtifactResolverPostProcessor
+ extends ArtifactResolverPostProcessorSupport
+{
+ public static final String NAME = "trusted-checksums";
+
+ private static final String CONF_CHECKSUM_ALGORITHMS =
"checksumAlgorithms";
+
+ private static final String DEFAULT_CHECKSUM_ALGORITHMS = "SHA-1";
+
+ private static final String CONF_FAIL_IF_MISSING = "failIfMissing";
+
+ private static final String CONF_RECORD = "record";
+
+ private static final String CHECKSUM_ALGORITHMS_CACHE_KEY =
+ TrustedChecksumsArtifactResolverPostProcessor.class.getName() +
".checksumAlgorithms";
+
+ private final ChecksumAlgorithmFactorySelector
checksumAlgorithmFactorySelector;
+
+ private final Map<String, TrustedChecksumsSource> trustedChecksumsSources;
+
+ @Inject
+ public TrustedChecksumsArtifactResolverPostProcessor(
+ ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector,
+ Map<String, TrustedChecksumsSource> trustedChecksumsSources )
+ {
+ super( NAME );
+ this.checksumAlgorithmFactorySelector = requireNonNull(
checksumAlgorithmFactorySelector );
+ this.trustedChecksumsSources = requireNonNull( trustedChecksumsSources
);
+ }
+
+ @SuppressWarnings( "unchecked" )
+ @Override
+ protected void doProcess( RepositorySystemSession session,
List<ArtifactResult> artifactResults )
+ {
+ final List<ChecksumAlgorithmFactory> checksumAlgorithms =
(List<ChecksumAlgorithmFactory>) session.getData()
+ .computeIfAbsent( CHECKSUM_ALGORITHMS_CACHE_KEY, () ->
+ checksumAlgorithmFactorySelector.select(
+ ConfigUtils.parseCommaSeparatedUniqueNames(
ConfigUtils.getString(
+ session, DEFAULT_CHECKSUM_ALGORITHMS,
CONF_CHECKSUM_ALGORITHMS ) )
+ ) );
+
+ final boolean failIfMissing = ConfigUtils.getBoolean(
+ session, false, configPropKey( CONF_FAIL_IF_MISSING ) );
+ final boolean record = ConfigUtils.getBoolean( session, false,
configPropKey( CONF_RECORD ) );
+
+ for ( ArtifactResult artifactResult : artifactResults )
+ {
+ if ( artifactResult.isResolved() )
+ {
+ if ( record )
+ {
+ recordArtifactChecksums( session, artifactResult,
checksumAlgorithms );
+ }
+ else if ( !validateArtifactChecksums( session, artifactResult,
checksumAlgorithms, failIfMissing ) )
+ {
+ artifactResult.setArtifact(
artifactResult.getArtifact().setFile( null ) ); // make it unresolved
+ }
+ }
+ }
+ }
+
+ /**
+ * Calculates and records checksums into trusted sources that support
writing.
+ */
+ private void recordArtifactChecksums( RepositorySystemSession session,
+ ArtifactResult artifactResult,
+ List<ChecksumAlgorithmFactory>
checksumAlgorithmFactories )
+ {
+ Artifact artifact = artifactResult.getArtifact();
+ ArtifactRepository artifactRepository = artifactResult.getRepository();
+
+ try
+ {
+ final Map<String, String> calculatedChecksums =
ChecksumAlgorithmHelper.calculate(
+ artifact.getFile(), checksumAlgorithmFactories );
+
+ for ( TrustedChecksumsSource trustedChecksumsSource :
trustedChecksumsSources.values() )
+ {
+ try ( TrustedChecksumsSource.Writer writer =
trustedChecksumsSource
+ .getTrustedArtifactChecksumsWriter( session ) )
+ {
+ if ( writer != null )
+ {
+ writer.addTrustedArtifactChecksums( artifact,
artifactRepository, checksumAlgorithmFactories,
+ calculatedChecksums );
+ }
+ }
+ }
+ }
+ catch ( IOException e )
+ {
+ throw new UncheckedIOException( "Could not calculate amd write
required checksums for "
+ + artifact.getFile(), e );
+ }
+ }
+
+ /**
+ * Validates trusted checksums against {@link ArtifactResult}, returns
{@code true} denoting "valid" checksums or
+ * {@code false} denoting "invalid" checksums.
+ */
+ private boolean validateArtifactChecksums( RepositorySystemSession session,
+ ArtifactResult artifactResult,
+ List<ChecksumAlgorithmFactory>
checksumAlgorithmFactories,
+ boolean failIfMissing )
+ {
+ Artifact artifact = artifactResult.getArtifact();
+ ArtifactRepository artifactRepository = artifactResult.getRepository();
+
+ boolean valid = true;
+ boolean validated = false;
+ try
+ {
+ // full set: calculate all algorithms we were asked for
+ final Map<String, String> calculatedChecksums =
ChecksumAlgorithmHelper.calculate(
+ artifact.getFile(), checksumAlgorithmFactories );
+
+ for ( Map.Entry<String, TrustedChecksumsSource> entry :
trustedChecksumsSources.entrySet() )
+ {
+ final String trustedSourceName = entry.getKey();
+ final TrustedChecksumsSource trustedChecksumsSource =
entry.getValue();
+
+ // upper bound set: ask source for checksums, ideally same as
calculatedChecksums but may be less
+ Map<String, String> trustedChecksums =
trustedChecksumsSource.getTrustedArtifactChecksums(
+ session, artifact, artifactRepository,
checksumAlgorithmFactories );
+
+ if ( trustedChecksums == null )
+ {
+ continue; // not enabled
+ }
+ validated = true;
+
+ if ( !calculatedChecksums.equals( trustedChecksums ) )
+ {
+ Set<String> missingTrustedAlg = new HashSet<>(
calculatedChecksums.keySet() );
+ missingTrustedAlg.removeAll( trustedChecksums.keySet() );
+
+ if ( !missingTrustedAlg.isEmpty() && failIfMissing )
+ {
+ artifactResult.addException( new
ChecksumFailureException( "Missing from " + trustedSourceName
+ + " trusted checksum(s) " + missingTrustedAlg
+ " for artifact "
+ + ArtifactIdUtils.toId( artifact ) ) );
+ valid = false;
+ }
+
+ // compare values but only present ones, failIfMissing
handled above
+ // we still want to report all: algX - missing, algY -
mismatch, etc
+ for ( ChecksumAlgorithmFactory checksumAlgorithmFactory :
checksumAlgorithmFactories )
+ {
+ String calculatedChecksum = calculatedChecksums.get(
checksumAlgorithmFactory.getName() );
+ String trustedChecksum = trustedChecksums.get(
checksumAlgorithmFactory.getName() );
+ if ( trustedChecksum != null && !Objects.equals(
calculatedChecksum, trustedChecksum ) )
+ {
+ artifactResult.addException( new
ChecksumFailureException( "Artifact "
+ + ArtifactIdUtils.toId( artifact ) + "
trusted checksum mismatch: "
+ + trustedSourceName + "=" +
trustedChecksum + "; calculated="
+ + calculatedChecksum ) );
+ valid = false;
+ }
+ }
+ }
+ }
+
+ if ( !validated && failIfMissing )
+ {
+ artifactResult.addException( new ChecksumFailureException(
"There are no enabled trusted checksums"
+ + " source(s) to validate against." ) );
+ valid = false;
+ }
+ }
+ catch ( IOException e )
+ {
+ throw new UncheckedIOException( e );
+ }
+ return valid;
+ }
+}
diff --git
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java
index 65ad4880..41600aaa 100644
---
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java
+++
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java
@@ -25,6 +25,7 @@ import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -103,6 +104,7 @@ public class DefaultArtifactResolverTest
resolver.setRemoteRepositoryManager( new StubRemoteRepositoryManager()
);
resolver.setSyncContextFactory( new StubSyncContextFactory() );
resolver.setOfflineController( new DefaultOfflineController() );
+ resolver.setArtifactResolverPostProcessors( Collections.emptyMap() );
artifact = new DefaultArtifact( "gid", "aid", "", "ext", "ver" );
diff --git
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceTestSupport.java
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceTestSupport.java
index 6a8d4e52..12f62b55 100644
---
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceTestSupport.java
+++
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceTestSupport.java
@@ -35,6 +35,7 @@ import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
public abstract class FileTrustedChecksumsSourceTestSupport
{
@@ -63,21 +64,38 @@ public abstract class FileTrustedChecksumsSourceTestSupport
protected abstract FileTrustedChecksumsSourceSupport prepareSubject( Path
basedir ) throws IOException;
+ protected abstract void enableSource();
+
+ @Test
+ public void notEnabled()
+ {
+ assertNull( subject.getTrustedArtifactChecksums(
+ session,
+ ARTIFACT_WITH_CHECKSUM,
+ session.getLocalRepository(),
+ Collections.singletonList( checksumAlgorithmFactory )
+ )
+ );
+ }
+
@Test
public void noProvidedArtifactChecksum()
{
+ enableSource();
Map<String, String> providedChecksums =
subject.getTrustedArtifactChecksums(
session,
ARTIFACT_WITHOUT_CHECKSUM,
session.getLocalRepository(),
Collections.singletonList( checksumAlgorithmFactory )
);
- assertNull( providedChecksums );
+ assertNotNull( providedChecksums );
+ assertTrue( providedChecksums.isEmpty() );
}
@Test
public void haveProvidedArtifactChecksum()
{
+ enableSource();
Map<String, String> providedChecksums =
subject.getTrustedArtifactChecksums(
session,
ARTIFACT_WITH_CHECKSUM,
diff --git
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
index f1645de8..1a9e8821 100644
---
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
+++
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
@@ -33,8 +33,6 @@ public class SparseDirectoryTrustedChecksumsSourceTest
extends FileTrustedChecks
@Override
protected FileTrustedChecksumsSourceSupport prepareSubject( Path basedir )
throws IOException
{
- session.setConfigProperty(
"aether.trustedChecksumsSource.sparse-directory",
- Boolean.TRUE.toString() );
LocalPathComposer localPathComposer = new DefaultLocalPathComposer();
// artifact: test:test:2.0 => "foobar"
{
@@ -47,4 +45,10 @@ public class SparseDirectoryTrustedChecksumsSourceTest
extends FileTrustedChecks
return new SparseDirectoryTrustedChecksumsSource( new
DefaultFileProcessor(), localPathComposer );
}
+
+ @Override
+ protected void enableSource()
+ {
+ session.setConfigProperty(
"aether.trustedChecksumsSource.sparse-directory", Boolean.TRUE.toString() );
+ }
}
diff --git
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
index a21f7ff9..9a292682 100644
---
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
+++
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
@@ -31,8 +31,6 @@ public class SummaryFileTrustedChecksumsSourceTest extends
FileTrustedChecksumsS
@Override
protected FileTrustedChecksumsSourceSupport prepareSubject( Path basedir )
throws IOException
{
- session.setConfigProperty(
"aether.trustedChecksumsSource.summary-file",
- Boolean.TRUE.toString() );
// artifact: test:test:2.0 => "foobar"
{
Path test = basedir.resolve( "checksums." +
checksumAlgorithmFactory.getFileExtension() );
@@ -44,4 +42,10 @@ public class SummaryFileTrustedChecksumsSourceTest extends
FileTrustedChecksumsS
return new SummaryFileTrustedChecksumsSource();
}
+
+ @Override
+ protected void enableSource()
+ {
+ session.setConfigProperty(
"aether.trustedChecksumsSource.summary-file", Boolean.TRUE.toString() );
+ }
}
diff --git
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessorTest.java
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessorTest.java
new file mode 100644
index 00000000..06cd721c
--- /dev/null
+++
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessorTest.java
@@ -0,0 +1,226 @@
+package org.eclipse.aether.internal.impl.resolution;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.internal.impl.checksum.Sha1ChecksumAlgorithmFactory;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
+import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
+import
org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactorySelector;
+import org.eclipse.aether.util.artifact.ArtifactIdUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+
+/**
+ * UT for {@link TrustedChecksumsArtifactResolverPostProcessor}.
+ */
+public class TrustedChecksumsArtifactResolverPostProcessorTest implements
TrustedChecksumsSource
+{
+ private static final String TRUSTED_SOURCE_NAME = "test";
+
+ private Artifact artifactWithoutTrustedChecksum;
+
+ private Artifact artifactWithTrustedChecksum;
+
+ private String artifactTrustedChecksum;
+
+ protected DefaultRepositorySystemSession session;
+
+ protected ChecksumAlgorithmFactory checksumAlgorithmFactory = new
Sha1ChecksumAlgorithmFactory();
+
+ private TrustedChecksumsArtifactResolverPostProcessor subject;
+
+ private TrustedChecksumsSource.Writer trustedChecksumsWriter;
+
+ @Before
+ public void prepareSubject() throws IOException
+ {
+ // make the two artifacts, BOTH as resolved
+ File tmp = Files.createTempFile( "artifact", "tmp" ).toFile();
+ artifactWithoutTrustedChecksum = new DefaultArtifact( "test:test:1.0"
).setFile( tmp );
+ artifactWithTrustedChecksum = new DefaultArtifact( "test:test:2.0"
).setFile( tmp );
+ artifactTrustedChecksum = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
// empty file
+
+ session = TestUtils.newSession();
+ ChecksumAlgorithmFactorySelector selector = new
ChecksumAlgorithmFactorySelector()
+ {
+ @Override
+ public ChecksumAlgorithmFactory select( String algorithmName )
+ {
+ if ( checksumAlgorithmFactory.getName().equals( algorithmName
) )
+ {
+ return checksumAlgorithmFactory;
+ }
+ throw new IllegalArgumentException("no alg factory for " +
algorithmName);
+ }
+
+ @Override
+ public Collection<ChecksumAlgorithmFactory>
getChecksumAlgorithmFactories()
+ {
+ return Collections.singletonList( checksumAlgorithmFactory );
+ }
+ };
+ subject = new TrustedChecksumsArtifactResolverPostProcessor( selector,
+ Collections.singletonMap( TRUSTED_SOURCE_NAME, this ) );
+ trustedChecksumsWriter = null;
+ session.setConfigProperty(
"aether.artifactResolver.postProcessor.trusted-checksums",
Boolean.TRUE.toString() );
+ }
+
+ // -- TrustedChecksumsSource interface BEGIN
+
+ @Override
+ public Map<String, String> getTrustedArtifactChecksums(
RepositorySystemSession session, Artifact artifact,
+ ArtifactRepository
artifactRepository,
+
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories )
+ {
+ if ( ArtifactIdUtils.toId( artifactWithTrustedChecksum ).equals(
ArtifactIdUtils.toId( artifact ) ) )
+ {
+ return Collections.singletonMap(
checksumAlgorithmFactory.getName(), artifactTrustedChecksum );
+ }
+ else
+ {
+ return Collections.emptyMap();
+ }
+ }
+
+ @Override
+ public Writer getTrustedArtifactChecksumsWriter( RepositorySystemSession
session )
+ {
+ return trustedChecksumsWriter;
+ }
+
+ // -- TrustedChecksumsSource interface END
+
+ private ArtifactResult createArtifactResult( Artifact artifact )
+ {
+ ArtifactResult artifactResult = new ArtifactResult( new
ArtifactRequest().setArtifact( artifact ) );
+ artifactResult.setArtifact( artifact );
+ return artifactResult;
+ }
+
+ // UTs below
+
+ @Test
+ public void haveMatchingChecksumPass()
+ {
+ ArtifactResult artifactResult = createArtifactResult(
artifactWithTrustedChecksum );
+ assertThat( artifactResult.isResolved(), equalTo( true ) );
+
+ subject.postProcess( session, Collections.singletonList(
artifactResult ) );
+ assertThat( artifactResult.isResolved(), equalTo( true ) );
+ }
+
+ @Test
+ public void haveNoChecksumPass()
+ {
+ ArtifactResult artifactResult = createArtifactResult(
artifactWithoutTrustedChecksum );
+ assertThat( artifactResult.isResolved(), equalTo( true ) );
+
+ subject.postProcess( session, Collections.singletonList(
artifactResult ) );
+ assertThat( artifactResult.isResolved(), equalTo( true ) );
+ }
+
+ @Test
+ public void haveNoChecksumFailIfMissingEnabledFail()
+ {
+ session.setConfigProperty(
"aether.artifactResolver.postProcessor.trusted-checksums.failIfMissing",
+ Boolean.TRUE.toString() );
+ ArtifactResult artifactResult = createArtifactResult(
artifactWithoutTrustedChecksum );
+ assertThat( artifactResult.isResolved(), equalTo( true ) );
+
+ subject.postProcess( session, Collections.singletonList(
artifactResult ) );
+ assertThat( artifactResult.isResolved(), equalTo( false ) );
+ assertThat( artifactResult.getExceptions(), not( empty() ) );
+ assertThat( artifactResult.getExceptions().get( 0 ).getMessage(),
+ containsString( "Missing from " + TRUSTED_SOURCE_NAME + "
trusted" ) );
+ }
+
+ @Test
+ public void haveMismatchingChecksumFail()
+ {
+ artifactTrustedChecksum = "foobar";
+ ArtifactResult artifactResult = createArtifactResult(
artifactWithTrustedChecksum );
+ assertThat( artifactResult.isResolved(), equalTo( true ) );
+
+ subject.postProcess( session, Collections.singletonList(
artifactResult ) );
+ assertThat( artifactResult.isResolved(), equalTo( false ) );
+ assertThat( artifactResult.getExceptions(), not( empty() ) );
+ assertThat( artifactResult.getExceptions().get( 0 ).getMessage(),
+ containsString( "trusted checksum mismatch" ) );
+ assertThat( artifactResult.getExceptions().get( 0 ).getMessage(),
+ containsString( TRUSTED_SOURCE_NAME + "=" +
artifactTrustedChecksum ) );
+ }
+
+ @Test
+ public void recordCalculatedChecksum()
+ {
+ AtomicReference<String> recordedChecksum = new AtomicReference<>(null);
+ this.trustedChecksumsWriter = new Writer()
+ {
+ @Override
+ public void addTrustedArtifactChecksums( Artifact artifact,
ArtifactRepository artifactRepository,
+
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
+ Map<String, String>
trustedArtifactChecksums )
+ {
+ recordedChecksum.set( trustedArtifactChecksums.get(
checksumAlgorithmFactory.getName() ) );
+ }
+
+ @Override
+ public void close()
+ {
+ // nop
+ }
+ };
+ session.setConfigProperty(
"aether.artifactResolver.postProcessor.trusted-checksums.record",
+ Boolean.TRUE.toString() );
+ ArtifactResult artifactResult = createArtifactResult(
artifactWithTrustedChecksum );
+ assertThat( artifactResult.isResolved(), equalTo( true ) );
+
+ subject.postProcess( session, Collections.singletonList(
artifactResult ) );
+ assertThat( artifactResult.isResolved(), equalTo( true ) );
+
+ String checksum = recordedChecksum.get();
+ assertThat( checksum, notNullValue() );
+ assertThat( checksum, equalTo( artifactTrustedChecksum ) );
+ }
+}
diff --git
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/TrustedChecksumsSource.java
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/TrustedChecksumsSource.java
index 47e220ae..5d36a585 100644
---
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/TrustedChecksumsSource.java
+++
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/checksums/TrustedChecksumsSource.java
@@ -19,6 +19,8 @@ package org.eclipse.aether.spi.checksums;
* under the License.
*/
+import java.io.Closeable;
+import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -39,16 +41,42 @@ import
org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
public interface TrustedChecksumsSource
{
/**
- * May return the trusted checksums (for given artifact) from trusted
source, or {@code null}.
+ * May return the trusted checksums (for given artifact) from trusted
source, or {@code null} if not enabled.
+ * Enabled trusted checksum source SHOULD return non-null (empty map)
result, when it has no data for given
+ * artifact. Empty map means in this case "no information", but how that
case is interpreted depends on consumer
+ * for trusted checksums.
*
* @param session The repository system session, never
{@code null}.
* @param artifact The artifact we want checksums for,
never {@code null}.
* @param artifactRepository The origin repository: local,
workspace, remote repository, never {@code null}.
* @param checksumAlgorithmFactories The checksum algorithms that are
expected, never {@code null}.
- * @return Map of expected checksums, or {@code null}.
+ * @return Map of expected checksums, or {@code null} if not enabled.
*/
Map<String, String> getTrustedArtifactChecksums( RepositorySystemSession
session,
Artifact artifact,
ArtifactRepository
artifactRepository,
List<ChecksumAlgorithmFactory> checksumAlgorithmFactories );
+
+ /**
+ * A writer that is able to write/add trusted checksums to this
implementation. Should be treated as a resource
+ * as underlying implementation may rely on being closed after not used
anymore.
+ */
+ interface Writer extends Closeable
+ {
+ /**
+ * Performs whatever implementation requires to "set"
(write/add/append) given map of trusted checksums.
+ * The passed in list of checksum algorithm factories and the map must
have equal size and mapping must
+ * contain all algorithm names in list.
+ */
+ void addTrustedArtifactChecksums( Artifact artifact,
+ ArtifactRepository
artifactRepository,
+ List<ChecksumAlgorithmFactory>
checksumAlgorithmFactories,
+ Map<String, String>
trustedArtifactChecksums ) throws IOException;
+ }
+
+ /**
+ * Some trusted checksums sources may implement this optional method:
ability to write/add checksums to them.
+ * If source does not support this feature, method should return {@code
null}.
+ */
+ Writer getTrustedArtifactChecksumsWriter( RepositorySystemSession session
);
}
diff --git
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumAlgorithmFactorySelector.java
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumAlgorithmFactorySelector.java
index 4b958a6d..5d89d40d 100644
---
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumAlgorithmFactorySelector.java
+++
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumAlgorithmFactorySelector.java
@@ -20,6 +20,9 @@ package org.eclipse.aether.spi.connector.checksum;
*/
import java.util.Collection;
+import java.util.List;
+
+import static java.util.stream.Collectors.toList;
/**
* Component performing selection of {@link ChecksumAlgorithmFactory} based on
known factory names.
@@ -35,6 +38,19 @@ public interface ChecksumAlgorithmFactorySelector
*/
ChecksumAlgorithmFactory select( String algorithmName );
+ /**
+ * Returns list of factories for given algorithm names in order as
collection is ordered, or throws if algorithm
+ * not supported.
+ *
+ * @throws IllegalArgumentException if asked algorithm name is not
supported.
+ * @throws NullPointerException if passed in list of names is {@code null}.
+ * @since TBD
+ */
+ default List<ChecksumAlgorithmFactory> select( Collection<String>
algorithmNames )
+ {
+ return algorithmNames.stream().map( this::select ).collect( toList() );
+ }
+
/**
* Returns a collection of supported algorithms. This set represents ALL
the algorithms supported by Resolver,
* and is NOT in any relation to given repository layout used checksums,
returned by method {@link
diff --git
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ProvidedChecksumsSource.java
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ProvidedChecksumsSource.java
index 3ba83e47..809fedee 100644
---
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ProvidedChecksumsSource.java
+++
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ProvidedChecksumsSource.java
@@ -34,10 +34,16 @@ import org.eclipse.aether.spi.connector.ArtifactDownload;
public interface ProvidedChecksumsSource
{
/**
- * May return the provided checksums (for given artifact transfer) from
trusted source other than remote
- * repository, or {@code null}.
+ * May return the provided checksums (for given artifact transfer) from
source other than remote repository, or
+ * {@code null} if it have no checksums available for given transfer.
Provided checksums are "opt-in" for
+ * transfer, in a way IF they are available upfront, they will be enforced
according to checksum policy
+ * in effect. Otherwise, provided checksum verification is completely left
out.
+ * <p>
+ * For enabled provided checksum source is completely acceptable to return
{@code null} values, as that carries
+ * the meaning "nothing to add here", as there are no checksums to be
provided upfront transfer. Semantically, this
+ * is equivalent to returning empty map, but signals the intent better.
*
- * @param transfer The transfer that is about to be executed.
+ * @param transfer The transfer that is about to be
executed.
* @param checksumAlgorithmFactories The checksum algorithms that are
expected.
* @return Map of expected checksums, or {@code null}.
*/
diff --git
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/resolution/ArtifactResolverPostProcessor.java
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/resolution/ArtifactResolverPostProcessor.java
new file mode 100644
index 00000000..990358b9
--- /dev/null
+++
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/resolution/ArtifactResolverPostProcessor.java
@@ -0,0 +1,47 @@
+package org.eclipse.aether.spi.resolution;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.resolution.ArtifactResult;
+
+/**
+ * Artifact resolver post-resolution processor component, is able to hook into
resolver and post-process the resolved
+ * artifact results, if needed even produce resolution failure. It will always
be invoked (even when failure is about
+ * to happen), so detecting these cases are left to post processor
implementations.
+ *
+ * @since TBD
+ */
+public interface ArtifactResolverPostProcessor
+{
+ /**
+ * Receives resolver results just before it would return it to caller. Is
able to generate "resolution failure"
+ * by augmenting passed in {@link ArtifactResult}s (artifacts should be
"unresolved" and exceptions added).
+ * <p>
+ * Implementations must be aware that the passed in list of {@link
ArtifactResult}s may have failed resolutions,
+ * best to check that using {@link ArtifactResult#isResolved()} method.
+ * <p>
+ * The implementations must be aware that this call may be "hot", so it
directly affects the performance of
+ * resolver in general.
+ */
+ void postProcess( RepositorySystemSession session, List<ArtifactResult>
artifactResults );
+}
diff --git
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
index df1ba3e3..ac4f38be 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
@@ -20,6 +20,7 @@ package org.eclipse.aether.util;
*/
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -27,6 +28,8 @@ import java.util.Map;
import org.eclipse.aether.RepositorySystemSession;
+import static java.util.stream.Collectors.toList;
+
/**
* A utility class to read configuration properties from a repository system
session.
*
@@ -395,4 +398,31 @@ public final class ConfigUtils
return getMap( session.getConfigProperties(), defaultValue, keys );
}
+ /**
+ * Utility method to parse configuration string that contains comma
separated list of names into
+ * {@link List<String>}, never returns {@code null}.
+ *
+ * @since TBD
+ */
+ public static List<String> parseCommaSeparatedNames( String
commaSeparatedNames )
+ {
+ if ( commaSeparatedNames == null ||
commaSeparatedNames.trim().isEmpty() )
+ {
+ return Collections.emptyList();
+ }
+ return Arrays.stream( commaSeparatedNames.split( "," ) )
+ .filter( s -> s != null && !s.trim().isEmpty() )
+ .collect( toList() );
+ }
+
+ /**
+ * Utility method to parse configuration string that contains comma
separated list of names into
+ * {@link List<String>} with unique elements (duplicates, if any, are
discarded), never returns {@code null}.
+ *
+ * @since TBD
+ */
+ public static List<String> parseCommaSeparatedUniqueNames( String
commaSeparatedNames )
+ {
+ return parseCommaSeparatedNames( commaSeparatedNames
).stream().distinct().collect( toList() );
+ }
}