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.Named;
022import javax.inject.Singleton;
023
024import org.eclipse.aether.artifact.Artifact;
025import org.eclipse.aether.metadata.Metadata;
026
027import static java.util.Objects.requireNonNull;
028import static org.eclipse.aether.util.PathUtils.validateArtifactComponents;
029import static org.eclipse.aether.util.PathUtils.validateMetadataComponents;
030
031/**
032 * Default implementation of {@link LocalPathComposer}.
033 *
034 * @since 1.8.1
035 */
036@Singleton
037@Named
038public final class DefaultLocalPathComposer implements LocalPathComposer {
039    @Override
040    public String getPathForArtifact(Artifact artifact, boolean local) {
041        requireNonNull(artifact);
042        validateArtifactComponents(artifact);
043
044        StringBuilder path = new StringBuilder(128);
045
046        path.append(artifact.getGroupId().replace('.', '/')).append('/');
047
048        path.append(artifact.getArtifactId()).append('/');
049
050        path.append(artifact.getBaseVersion()).append('/');
051
052        path.append(artifact.getArtifactId()).append('-');
053        if (local) {
054            path.append(artifact.getBaseVersion());
055        } else {
056            path.append(artifact.getVersion());
057        }
058
059        if (!artifact.getClassifier().isEmpty()) {
060            path.append('-').append(artifact.getClassifier());
061        }
062
063        if (!artifact.getExtension().isEmpty()) {
064            path.append('.').append(artifact.getExtension());
065        }
066
067        return requireContainedPath(path.toString());
068    }
069
070    @Override
071    public String getPathForMetadata(Metadata metadata, String repositoryKey) {
072        requireNonNull(metadata);
073        requireNonNull(repositoryKey);
074        validateMetadataComponents(metadata);
075
076        StringBuilder path = new StringBuilder(128);
077
078        if (!metadata.getGroupId().isEmpty()) {
079            path.append(metadata.getGroupId().replace('.', '/')).append('/');
080
081            if (!metadata.getArtifactId().isEmpty()) {
082                path.append(metadata.getArtifactId()).append('/');
083
084                if (!metadata.getVersion().isEmpty()) {
085                    path.append(metadata.getVersion()).append('/');
086                }
087            }
088        }
089
090        path.append(insertRepositoryKey(metadata.getType(), repositoryKey));
091
092        return requireContainedPath(path.toString());
093    }
094
095    /**
096     * Defense in depth: the composed path must be relative and must not contain parent-reference or empty
097     * segments, so that resolving it against the local repository base directory always yields a path within
098     * it. The coordinate validation performed on entry should already guarantee this; this is a last-resort
099     * check on the composed result.
100     */
101    private static String requireContainedPath(String path) {
102        if (path.startsWith("/")
103                || path.contains("//")
104                || path.equals("..")
105                || path.startsWith("../")
106                || path.endsWith("/..")
107                || path.contains("/../")) {
108            throw new IllegalArgumentException(
109                    "Invalid coordinates: composed local repository path is not a valid relative path: " + path);
110        }
111        return path;
112    }
113
114    private String insertRepositoryKey(String metadataType, String repositoryKey) {
115        if (metadataType.contains("/") && !metadataType.endsWith("/")) {
116            int lastSlash = metadataType.lastIndexOf('/');
117            return metadataType.substring(0, lastSlash + 1)
118                    + insertRepositoryKey(metadataType.substring(lastSlash + 1), repositoryKey);
119        } else {
120            String result;
121            int idx = metadataType.indexOf('.');
122            if (idx < 0) {
123                result = metadataType + '-' + repositoryKey;
124            } else {
125                result = metadataType.substring(0, idx) + '-' + repositoryKey + metadataType.substring(idx);
126            }
127            return result;
128        }
129    }
130}