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}