1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package libcore.net.url; 18 19 import java.util.Locale; 20 21 public final class UrlUtils { UrlUtils()22 private UrlUtils() { 23 } 24 25 /** 26 * Returns the path will relative path segments like ".." and "." resolved. 27 * The returned path will not necessarily start with a "/" character. This 28 * handles ".." and "." segments at both the beginning and end of the path. 29 * 30 * @param discardRelativePrefix true to remove leading ".." segments from 31 * the path. This is appropriate for paths that are known to be 32 * absolute. 33 */ canonicalizePath(String path, boolean discardRelativePrefix)34 public static String canonicalizePath(String path, boolean discardRelativePrefix) { 35 // the first character of the current path segment 36 int segmentStart = 0; 37 38 // the number of segments seen thus far that can be erased by sequences of '..'. 39 int deletableSegments = 0; 40 41 for (int i = 0; i <= path.length(); ) { 42 int nextSegmentStart; 43 if (i == path.length()) { 44 nextSegmentStart = i; 45 } else if (path.charAt(i) == '/') { 46 nextSegmentStart = i + 1; 47 } else { 48 i++; 49 continue; 50 } 51 52 /* 53 * We've encountered either the end of a segment or the end of the 54 * complete path. If the final segment was "." or "..", remove the 55 * appropriate segments of the path. 56 */ 57 if (i == segmentStart + 1 && path.regionMatches(segmentStart, ".", 0, 1)) { 58 // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi". 59 path = path.substring(0, segmentStart) + path.substring(nextSegmentStart); 60 i = segmentStart; 61 } else if (i == segmentStart + 2 && path.regionMatches(segmentStart, "..", 0, 2)) { 62 if (deletableSegments > 0 || discardRelativePrefix) { 63 // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi". 64 deletableSegments--; 65 int prevSegmentStart = path.lastIndexOf('/', segmentStart - 2) + 1; 66 path = path.substring(0, prevSegmentStart) + path.substring(nextSegmentStart); 67 i = segmentStart = prevSegmentStart; 68 } else { 69 // There's no segment to delete; this ".." segment must be retained. 70 i++; 71 segmentStart = i; 72 } 73 } else { 74 if (i > 0) { 75 deletableSegments++; 76 } 77 i++; 78 segmentStart = i; 79 } 80 } 81 return path; 82 } 83 84 /** 85 * Returns a path that can be safely concatenated with {@code authority}. If 86 * the authority is null or empty, this can be any path. Otherwise the paths 87 * run together like {@code http://android.comindex.html}. 88 */ authoritySafePath(String authority, String path)89 public static String authoritySafePath(String authority, String path) { 90 if (authority != null && !authority.isEmpty() && !path.isEmpty() && !path.startsWith("/")) { 91 return "/" + path; 92 } 93 return path; 94 } 95 96 /** 97 * Returns the scheme prefix like "http" from the URL spec, or null if the 98 * spec doesn't start with a scheme. Scheme prefixes match this pattern: 99 * {@code alpha ( alpha | digit | '+' | '-' | '.' )* ':'} 100 */ getSchemePrefix(String spec)101 public static String getSchemePrefix(String spec) { 102 int colon = spec.indexOf(':'); 103 104 if (colon < 1) { 105 return null; 106 } 107 108 for (int i = 0; i < colon; i++) { 109 char c = spec.charAt(i); 110 if (!isValidSchemeChar(i, c)) { 111 return null; 112 } 113 } 114 115 return spec.substring(0, colon).toLowerCase(Locale.US); 116 } 117 isValidSchemeChar(int index, char c)118 public static boolean isValidSchemeChar(int index, char c) { 119 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { 120 return true; 121 } 122 if (index > 0 && ((c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.')) { 123 return true; 124 } 125 return false; 126 } 127 128 /** 129 * Returns the index of the first char of {@code chars} in {@code string} 130 * bounded between {@code start} and {@code end}. This returns {@code end} 131 * if none of the characters exist in the requested range. 132 */ findFirstOf(String string, String chars, int start, int end)133 public static int findFirstOf(String string, String chars, int start, int end) { 134 for (int i = start; i < end; i++) { 135 char c = string.charAt(i); 136 if (chars.indexOf(c) != -1) { 137 return i; 138 } 139 } 140 return end; 141 } 142 } 143