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.repository; 020 021import java.net.URI; 022import java.net.URISyntaxException; 023import java.nio.file.Paths; 024 025/** 026 * URL handling for file URLs. Based on org.apache.maven.wagon.PathUtils. 027 * 028 * @since 2.0.0 029 */ 030public final class RepositoryUriUtils { 031 032 private RepositoryUriUtils() {} 033 034 /** 035 * Converts the given repository URL or file path to a {@link URI}, resolving plain file paths and 036 * {@code file:} URLs against the local file system. 037 * 038 * @param repositoryUrl The repository URL or file path to convert, must not be {@code null}. 039 * @return The resulting URI, never {@code null}. 040 */ 041 public static URI toUri(String repositoryUrl) { 042 final String protocol = protocol(repositoryUrl); 043 if ("file".equals(protocol)) { 044 // Use basedir() to normalize and extract the path from any file: URL form: 045 // opaque (file:repo), with authority (file://host/path), unencoded spaces, 046 // percent-encoded spaces, Windows drive letters (file:/C:/..., file:///C:/...). 047 // Then rebuild a well-formed file: URI using the multi-arg URI constructor 048 // (which percent-encodes the path automatically), and pass it to Paths.get(URI) 049 // so that Windows drive-letter handling is done by the JDK. 050 String path = basedir(repositoryUrl); 051 try { 052 URI uri; 053 if (path.isEmpty() 054 || (!path.startsWith("/") && !isWindowsDriveLetter(String.valueOf(path.charAt(0))))) { 055 // Relative path (e.g. from opaque file:repo): resolve against cwd via Paths.get(String) 056 return Paths.get(path).toAbsolutePath().toUri(); 057 } 058 // Absolute path or Windows drive letter path. 059 // Use authority="" to produce file:///path (three-slash form) which Paths.get(URI) 060 // handles correctly on Windows: the JDK treats the empty authority as localhost 061 // and strips the leading '/' before a Windows drive letter (e.g. /C:/... → C:/...). 062 uri = new URI("file", "", path, null, null); 063 return Paths.get(uri).toUri(); 064 } catch (URISyntaxException e) { 065 // Should not happen since basedir() returns a clean path; fall back to string-based parse 066 return Paths.get(path).toUri(); 067 } 068 } else if (protocol.isEmpty() || isWindowsDriveLetter(protocol)) { 069 // Use Paths.get() to parse the local path lexically, without resolving against the JVM 070 // working directory via File (which may be on a different drive on Windows). 071 return Paths.get(basedir(repositoryUrl)).toUri(); 072 } else { 073 return URI.create(repositoryUrl); 074 } 075 } 076 077 /** 078 * Return the protocol name. 079 * 080 * @param url the url 081 * @return the protocol or empty string. 082 */ 083 private static String protocol(final String url) { 084 final int pos = url.indexOf(":"); 085 086 if (pos == -1) { 087 return ""; 088 } 089 return url.substring(0, pos).trim(); 090 } 091 092 /** 093 * Derive the path portion of the given URL. 094 * 095 * @param url the file-repository URL 096 * @return the basedir of the repository 097 */ 098 private static String basedir(String url) { 099 String protocol = protocol(url); 100 101 String retValue; 102 103 if (!protocol.isEmpty()) { 104 // A single letter (case-insensitive) is a Windows drive letter (e.g. "C:\path" or "c:\path"), 105 // not a URL scheme. Keep it in the path; do not strip it as if it were a protocol prefix. 106 if (isWindowsDriveLetter(protocol)) { 107 retValue = url; 108 } else { 109 retValue = url.substring(protocol.length() + 1); 110 } 111 } else { 112 retValue = url; 113 } 114 retValue = decode(retValue); 115 // special case: if omitted // on protocol, keep path as is 116 if (retValue.startsWith("//")) { 117 retValue = retValue.substring(2); 118 119 if (retValue.length() >= 2 && (retValue.charAt(1) == '|' || retValue.charAt(1) == ':')) { 120 // special case: if there is a windows drive letter, then keep the original return value 121 retValue = retValue.charAt(0) + ":" + retValue.substring(2); 122 } else { 123 // Now we expect the host 124 int index = retValue.indexOf("/"); 125 if (index >= 0) { 126 retValue = retValue.substring(index + 1); 127 } 128 129 // special case: if there is a windows drive letter, then keep the original return value 130 if (retValue.length() >= 2 && (retValue.charAt(1) == '|' || retValue.charAt(1) == ':')) { 131 retValue = retValue.charAt(0) + ":" + retValue.substring(2); 132 } else if (index >= 0) { 133 // leading / was previously stripped 134 retValue = "/" + retValue; 135 } 136 } 137 } 138 139 // special case: if there is a windows drive letter using |, switch to : 140 if (retValue.length() >= 2 && retValue.charAt(1) == '|') { 141 retValue = retValue.charAt(0) + ":" + retValue.substring(2); 142 } 143 144 return retValue.trim(); 145 } 146 147 /** 148 * Returns {@code true} if the given protocol token is a single ASCII letter, which on Windows indicates 149 * a drive letter (e.g. {@code C} in {@code C:\\path} or {@code c} in {@code c:/path}) rather than a URL scheme. 150 * The check is restricted to ASCII letters only (not Unicode letters) and is locale-independent. 151 */ 152 private static boolean isWindowsDriveLetter(String protocol) { 153 if (protocol.length() != 1) { 154 return false; 155 } 156 char c = protocol.charAt(0); 157 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); 158 } 159 160 /** 161 * Decodes the specified (portion of a) URL. <strong>Note:</strong> This decoder assumes that ISO-8859-1 is used to 162 * convert URL-encoded octets to characters. 163 * 164 * @param url The URL to decode, may be <code>null</code>. 165 * @return The decoded URL or <code>null</code> if the input was <code>null</code>. 166 */ 167 private static String decode(String url) { 168 String decoded = url; 169 if (url != null) { 170 int pos = -1; 171 while ((pos = decoded.indexOf('%', pos + 1)) >= 0) { 172 if (pos + 2 < decoded.length()) { 173 String hexStr = decoded.substring(pos + 1, pos + 3); 174 char ch = (char) Integer.parseInt(hexStr, 16); 175 decoded = decoded.substring(0, pos) + ch + decoded.substring(pos + 3); 176 } 177 } 178 } 179 return decoded; 180 } 181}