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.util;
020
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.Map;
024
025import org.eclipse.aether.artifact.Artifact;
026import org.eclipse.aether.metadata.Metadata;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * A reusable utility class for file paths.
032 *
033 * @since 2.0.13
034 */
035public final class PathUtils {
036    private PathUtils() {
037        // hide constructor
038    }
039
040    private static final Map<String, String> ILLEGAL_PATH_SEGMENT_REPLACEMENTS;
041
042    static {
043        HashMap<String, String> illegalPathSegmentReplacements = new HashMap<>();
044        illegalPathSegmentReplacements.put("\\", "-BACKSLASH-");
045        illegalPathSegmentReplacements.put("/", "-SLASH-");
046        illegalPathSegmentReplacements.put(":", "-COLON-");
047        illegalPathSegmentReplacements.put("\"", "-QUOTE-");
048        illegalPathSegmentReplacements.put("<", "-LT-");
049        illegalPathSegmentReplacements.put(">", "-GT-");
050        illegalPathSegmentReplacements.put("|", "-PIPE-");
051        illegalPathSegmentReplacements.put("?", "-QMARK-");
052        illegalPathSegmentReplacements.put("*", "-ASTERISK-");
053        ILLEGAL_PATH_SEGMENT_REPLACEMENTS = Collections.unmodifiableMap(illegalPathSegmentReplacements);
054    }
055
056    /**
057     * Method that makes sure that passed in string is valid "path segment" string. It achieves it by potentially
058     * changing it, replacing illegal characters in it with legal ones.
059     * <p>
060     * Note: this method considers empty string as "valid path segment", it is caller duty to ensure empty string
061     * is not used as path segment alone.
062     * <p>
063     * This method is simplistic on purpose, and if frequently used, best if results are cached (per session)
064     */
065    public static String stringToPathSegment(String string) {
066        requireNonNull(string);
067        StringBuilder result = new StringBuilder(string);
068        for (Map.Entry<String, String> entry : ILLEGAL_PATH_SEGMENT_REPLACEMENTS.entrySet()) {
069            String illegal = entry.getKey();
070            int pos = result.indexOf(illegal);
071            while (pos >= 0) {
072                result.replace(pos, pos + illegal.length(), entry.getValue());
073                pos = result.indexOf(illegal);
074            }
075        }
076        return result.toString();
077    }
078
079    /**
080     * Validates that a coordinate component does not contain path traversal sequences
081     * or path separator characters that could cause the composed path to escape
082     * the local repository directory.
083     *
084     * @since 2.0.21
085     */
086    public static void validatePathComponent(String value, String label) {
087        if (value != null && !value.isEmpty()) {
088            // Important: "equals .." and not "contains ..", as if escape attempted, it will contain path separators
089            // OTOH: version "1.." is valid version string!
090            if (value.equals("..") || value.contains("/") || value.contains("\\")) {
091                throw new IllegalArgumentException(
092                        "Invalid " + label + ": must not contain '..', '/' or '\\': " + value);
093            }
094        }
095    }
096
097    /**
098     * Validates all coordinate components of an {@link Artifact}.
099     *
100     * @see #validatePathComponent(String, String)
101     * @since 2.0.21
102     */
103    public static void validateArtifactComponents(Artifact artifact) {
104        validatePathComponent(artifact.getGroupId(), "groupId");
105        validatePathComponent(artifact.getArtifactId(), "artifactId");
106        validatePathComponent(artifact.getVersion(), "version");
107        validatePathComponent(artifact.getBaseVersion(), "baseVersion");
108        validatePathComponent(artifact.getClassifier(), "classifier");
109        validatePathComponent(artifact.getExtension(), "extension");
110    }
111
112    /**
113     * Validates all coordinate components of a {@link Metadata}.
114     *
115     * @see #validatePathComponent(String, String)
116     * @since 2.0.21
117     */
118    public static void validateMetadataComponents(Metadata metadata) {
119        validatePathComponent(metadata.getGroupId(), "groupId");
120        validatePathComponent(metadata.getArtifactId(), "artifactId");
121        validatePathComponent(metadata.getVersion(), "version");
122        // note: type may contain string like ".meta/prefixes.txt"!
123    }
124}