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;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.io.IOException;
026
027import org.eclipse.aether.ConfigurationProperties;
028import org.eclipse.aether.RepositorySystemSession;
029import org.eclipse.aether.repository.LocalRepository;
030import org.eclipse.aether.repository.LocalRepositoryManager;
031import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
032import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
033import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
034import org.eclipse.aether.util.ConfigUtils;
035
036import static java.util.Objects.requireNonNull;
037
038/**
039 * Creates enhanced local repository managers for repository types {@code "default"} or {@code "" (automatic)}. Enhanced
040 * local repository manager is built upon the classical Maven 2.0 local repository structure but additionally keeps
041 * track of from what repositories a cached artifact was resolved. Resolution of locally cached artifacts will be
042 * rejected in case the current resolution request does not match the known source repositories of an artifact, thereby
043 * emulating physically separated artifact caches per remote repository.
044 */
045@Singleton
046@Named(EnhancedLocalRepositoryManagerFactory.NAME)
047public class EnhancedLocalRepositoryManagerFactory implements LocalRepositoryManagerFactory {
048    public static final String NAME = "enhanced";
049
050    static final String CONFIG_PROPS_PREFIX = ConfigurationProperties.PREFIX_LRM + NAME + ".";
051
052    /**
053     * Filename of the file in which to track the remote repositories.
054     *
055     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
056     * @configurationType {@link java.lang.String}
057     * @configurationDefaultValue {@link #DEFAULT_TRACKING_FILENAME}
058     */
059    public static final String CONFIG_PROP_TRACKING_FILENAME = CONFIG_PROPS_PREFIX + "trackingFilename";
060
061    public static final String DEFAULT_TRACKING_FILENAME = "_remote.repositories";
062
063    /**
064     * Whether to verify that the real (on-disk) path of a locally cached artifact matches the requested path
065     * spelling before the artifact is used. On case-insensitive or case/normalization-preserving filesystems (the
066     * macOS and Windows defaults) a file cached for one set of coordinates also answers lookups for coordinates
067     * that differ only in case or Unicode normalization, while the repository tracking data is compared exactly:
068     * such an aliased file is treated as present-but-untracked and accepted with no download and no checksum
069     * verification, letting case-colliding coordinates poison distinct GAVs. When enabled (the default), an
070     * artifact whose on-disk path spelling differs from the requested one is treated as not present, forcing a
071     * proper download. Disable only if the local repository intentionally contains symbolic links below its base
072     * directory (a symlinked base directory itself is supported either way).
073     *
074     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
075     * @configurationType {@link java.lang.Boolean}
076     * @configurationDefaultValue {@link #DEFAULT_VERIFY_REAL_PATH}
077     * @since 2.0.23
078     */
079    public static final String CONFIG_PROP_VERIFY_REAL_PATH = CONFIG_PROPS_PREFIX + "verifyRealPath";
080
081    public static final boolean DEFAULT_VERIFY_REAL_PATH = true;
082
083    /**
084     * Marks whether the local repository is meant to be shared (or was shared) with legacy Maven 3.9 or older
085     * versions. Maven 3.9 and older versions suffer from "impostor" problem, where artifact and metadata origin was
086     * tracked only by the remote repository ID, where two remote repositories may share same ID but different URLs,
087     * in fact they may be completely unrelated to each other (ID clash by mistake), or, it may be due some sort of
088     * "impostor" attempt, where a malicious repository may pretend like some other repository.
089     * Right now, we intentionally default to {@code true} to ease users transitioning, and Resolver 2 will retain
090     * this "old" behavior (will observe legacy tracking entries and will store remote metadata as before). But,
091     * at some point in the future, the default value will be changed to {@code false} (and same change is warmly
092     * recommended for modern Maven users, who do not intend to share local repository with older Maven versions).
093     * When this configuration set to {@code false}, the "repository key" is not ID only anymore, but is changed
094     * to {@code $id-sha1($url)} form, and this key is used in "origin tracking" entries and in caching remote
095     * Maven Repository Metadata XML files as well, guaranteeing they are not mixed in case of same IDs.
096     *
097     * @see ConfigurationProperties#REPOSITORY_SYSTEM_REPOSITORY_KEY_FUNCTION
098     * @see ConfigurationProperties#REPOSITORY_TRACKING_REPOSITORY_KEY_FUNCTION
099     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
100     * @configurationType {@link java.lang.Boolean}
101     * @configurationDefaultValue {@link #DEFAULT_LEGACY_LOCAL_REPOSITORY}
102     * @since 2.0.23
103     */
104    public static final String CONFIG_PROP_LEGACY_LOCAL_REPOSITORY = CONFIG_PROPS_PREFIX + "legacyLocalRepository";
105
106    public static final boolean DEFAULT_LEGACY_LOCAL_REPOSITORY = true;
107
108    private float priority = 10.0f;
109
110    private final LocalPathComposer localPathComposer;
111
112    private final TrackingFileManager trackingFileManager;
113
114    private final LocalPathPrefixComposerFactory localPathPrefixComposerFactory;
115
116    private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
117
118    @Inject
119    public EnhancedLocalRepositoryManagerFactory(
120            final LocalPathComposer localPathComposer,
121            final TrackingFileManager trackingFileManager,
122            final LocalPathPrefixComposerFactory localPathPrefixComposerFactory,
123            final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory) {
124        this.localPathComposer = requireNonNull(localPathComposer);
125        this.trackingFileManager = requireNonNull(trackingFileManager);
126        this.localPathPrefixComposerFactory = requireNonNull(localPathPrefixComposerFactory);
127        this.repositoryKeyFunctionFactory = requireNonNull(repositoryKeyFunctionFactory);
128    }
129
130    @Override
131    public LocalRepositoryManager newInstance(RepositorySystemSession session, LocalRepository repository)
132            throws NoLocalRepositoryManagerException {
133        requireNonNull(session, "session cannot be null");
134        requireNonNull(repository, "repository cannot be null");
135
136        String trackingFilename = ConfigUtils.getString(session, "", CONFIG_PROP_TRACKING_FILENAME);
137        if (trackingFilename.isEmpty()
138                || trackingFilename.contains("/")
139                || trackingFilename.contains("\\")
140                || trackingFilename.contains("..")) {
141            trackingFilename = DEFAULT_TRACKING_FILENAME;
142        }
143        boolean legacyLocalRepository =
144                ConfigUtils.getBoolean(session, DEFAULT_LEGACY_LOCAL_REPOSITORY, CONFIG_PROP_LEGACY_LOCAL_REPOSITORY);
145
146        if ("".equals(repository.getContentType()) || "default".equals(repository.getContentType())) {
147            try {
148                return new EnhancedLocalRepositoryManager(
149                        repository.getBasePath(),
150                        localPathComposer,
151                        repositoryKeyFunctionFactory.trackingRepositoryKeyFunction(session),
152                        trackingFilename,
153                        legacyLocalRepository,
154                        trackingFileManager,
155                        localPathPrefixComposerFactory.createComposer(session));
156            } catch (IOException e) {
157                throw new NoLocalRepositoryManagerException(repository, e);
158            }
159        } else {
160            throw new NoLocalRepositoryManagerException(repository);
161        }
162    }
163
164    @Override
165    public float getPriority() {
166        return priority;
167    }
168
169    /**
170     * Sets the priority of this component.
171     *
172     * @param priority The priority.
173     * @return This component for chaining, never {@code null}.
174     */
175    public EnhancedLocalRepositoryManagerFactory setPriority(float priority) {
176        this.priority = priority;
177        return this;
178    }
179}