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        // Strings consisting solely of dots contain no illegal character, yet "." and ".." carry path
077        // meaning when used as a path segment. Map them to explicit tokens, mirroring the character
078        // replacements above.
079        String segment = result.toString();
080        if (segment.equals(".")) {
081            return "-DOT-";
082        } else if (segment.equals("..")) {
083            return "-DOTDOT-";
084        }
085        return segment;
086    }
087
088    /**
089     * Validates that a coordinate component does not contain path traversal sequences
090     * or path separator characters that could cause the composed path to escape
091     * the local repository directory.
092     *
093     * @since 2.0.21
094     */
095    public static void validatePathComponent(String value, String label) {
096        if (value != null && !value.isEmpty()) {
097            // Important: "equals .." and not "contains ..", as if escape attempted, it will contain path separators
098            // OTOH: version "1.." is valid version string!
099            // Colon is not a valid character in a coordinate component.
100            if (value.equals("..") || value.contains("/") || value.contains("\\") || value.contains(":")) {
101                throw new IllegalArgumentException(
102                        "Invalid " + label + ": must not contain '..', '/', '\\' or ':': " + value);
103            }
104        }
105    }
106
107    /**
108     * Validates a coordinate component that is expanded into multiple path segments by replacing each dot with a
109     * path separator, like the group ID is. Beside the checks done by
110     * {@link #validatePathComponent(String, String)}, it rejects values containing empty dot-separated segments
111     * (leading, trailing or consecutive dots), as the expansion of such values does not compose a valid
112     * relative path.
113     *
114     * @since 2.0.23
115     */
116    public static void validateDotSeparatedPathComponent(String value, String label) {
117        validatePathComponent(value, label);
118        if (value != null && !value.isEmpty()) {
119            for (String segment : value.split("\\.", -1)) {
120                if (segment.isEmpty()) {
121                    throw new IllegalArgumentException("Invalid " + label
122                            + ": must not contain empty segments (leading, trailing or consecutive dots): " + value);
123                }
124            }
125        }
126    }
127
128    /**
129     * Validates all coordinate components of an {@link Artifact}.
130     *
131     * @see #validatePathComponent(String, String)
132     * @see #validateDotSeparatedPathComponent(String, String)
133     * @since 2.0.21
134     */
135    public static void validateArtifactComponents(Artifact artifact) {
136        validateDotSeparatedPathComponent(artifact.getGroupId(), "groupId");
137        validatePathComponent(artifact.getArtifactId(), "artifactId");
138        validatePathComponent(artifact.getVersion(), "version");
139        validatePathComponent(artifact.getBaseVersion(), "baseVersion");
140        validatePathComponent(artifact.getClassifier(), "classifier");
141        validatePathComponent(artifact.getExtension(), "extension");
142    }
143
144    /**
145     * Validates all coordinate components of a {@link Metadata}.
146     *
147     * @see #validatePathComponent(String, String)
148     * @see #validateDotSeparatedPathComponent(String, String)
149     * @since 2.0.21
150     */
151    public static void validateMetadataComponents(Metadata metadata) {
152        validateDotSeparatedPathComponent(metadata.getGroupId(), "groupId");
153        validatePathComponent(metadata.getArtifactId(), "artifactId");
154        validatePathComponent(metadata.getVersion(), "version");
155        // note: type may contain string like ".meta/prefixes.txt"!
156    }
157}