1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.eclipse.aether.util;
20
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.Map;
24
25 import org.eclipse.aether.artifact.Artifact;
26 import org.eclipse.aether.metadata.Metadata;
27
28 import static java.util.Objects.requireNonNull;
29
30 /**
31 * A reusable utility class for file paths.
32 *
33 * @since 2.0.13
34 */
35 public final class PathUtils {
36 private PathUtils() {
37 // hide constructor
38 }
39
40 private static final Map<String, String> ILLEGAL_PATH_SEGMENT_REPLACEMENTS;
41
42 static {
43 HashMap<String, String> illegalPathSegmentReplacements = new HashMap<>();
44 illegalPathSegmentReplacements.put("\\", "-BACKSLASH-");
45 illegalPathSegmentReplacements.put("/", "-SLASH-");
46 illegalPathSegmentReplacements.put(":", "-COLON-");
47 illegalPathSegmentReplacements.put("\"", "-QUOTE-");
48 illegalPathSegmentReplacements.put("<", "-LT-");
49 illegalPathSegmentReplacements.put(">", "-GT-");
50 illegalPathSegmentReplacements.put("|", "-PIPE-");
51 illegalPathSegmentReplacements.put("?", "-QMARK-");
52 illegalPathSegmentReplacements.put("*", "-ASTERISK-");
53 ILLEGAL_PATH_SEGMENT_REPLACEMENTS = Collections.unmodifiableMap(illegalPathSegmentReplacements);
54 }
55
56 /**
57 * Method that makes sure that passed in string is valid "path segment" string. It achieves it by potentially
58 * changing it, replacing illegal characters in it with legal ones.
59 * <p>
60 * Note: this method considers empty string as "valid path segment", it is caller duty to ensure empty string
61 * is not used as path segment alone.
62 * <p>
63 * This method is simplistic on purpose, and if frequently used, best if results are cached (per session)
64 */
65 public static String stringToPathSegment(String string) {
66 requireNonNull(string);
67 StringBuilder result = new StringBuilder(string);
68 for (Map.Entry<String, String> entry : ILLEGAL_PATH_SEGMENT_REPLACEMENTS.entrySet()) {
69 String illegal = entry.getKey();
70 int pos = result.indexOf(illegal);
71 while (pos >= 0) {
72 result.replace(pos, pos + illegal.length(), entry.getValue());
73 pos = result.indexOf(illegal);
74 }
75 }
76 // Strings consisting solely of dots contain no illegal character, yet "." and ".." carry path
77 // meaning when used as a path segment. Map them to explicit tokens, mirroring the character
78 // replacements above.
79 String segment = result.toString();
80 if (segment.equals(".")) {
81 return "-DOT-";
82 } else if (segment.equals("..")) {
83 return "-DOTDOT-";
84 }
85 return segment;
86 }
87
88 /**
89 * Validates that a coordinate component does not contain path traversal sequences
90 * or path separator characters that could cause the composed path to escape
91 * the local repository directory.
92 *
93 * @since 2.0.21
94 */
95 public static void validatePathComponent(String value, String label) {
96 if (value != null && !value.isEmpty()) {
97 // Important: "equals .." and not "contains ..", as if escape attempted, it will contain path separators
98 // OTOH: version "1.." is valid version string!
99 // 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 }