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}