1 /* 2 * Copyright (C) 2016 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 package com.google.android.exoplayer2.util; 17 18 import android.net.Uri; 19 import android.text.TextUtils; 20 import androidx.annotation.Nullable; 21 22 /** 23 * Utility methods for manipulating URIs. 24 */ 25 public final class UriUtil { 26 27 /** 28 * The length of arrays returned by {@link #getUriIndices(String)}. 29 */ 30 private static final int INDEX_COUNT = 4; 31 /** 32 * An index into an array returned by {@link #getUriIndices(String)}. 33 * <p> 34 * The value at this position in the array is the index of the ':' after the scheme. Equals -1 if 35 * the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1), 36 * including when the URI has no scheme. 37 */ 38 private static final int SCHEME_COLON = 0; 39 /** 40 * An index into an array returned by {@link #getUriIndices(String)}. 41 * <p> 42 * The value at this position in the array is the index of the path part. Equals (schemeColon + 1) 43 * if no authority part, (schemeColon + 3) if the authority part consists of just "//", and 44 * (query) if no path part. The characters starting at this index can be "//" only if the 45 * authority part is non-empty (in this case the double-slash means the first segment is empty). 46 */ 47 private static final int PATH = 1; 48 /** 49 * An index into an array returned by {@link #getUriIndices(String)}. 50 * <p> 51 * The value at this position in the array is the index of the query part, including the '?' 52 * before the query. Equals fragment if no query part, and (fragment - 1) if the query part is a 53 * single '?' with no data. 54 */ 55 private static final int QUERY = 2; 56 /** 57 * An index into an array returned by {@link #getUriIndices(String)}. 58 * <p> 59 * The value at this position in the array is the index of the fragment part, including the '#' 60 * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if 61 * the fragment part is a single '#' with no data. 62 */ 63 private static final int FRAGMENT = 3; 64 UriUtil()65 private UriUtil() {} 66 67 /** 68 * Like {@link #resolve(String, String)}, but returns a {@link Uri} instead of a {@link String}. 69 * 70 * @param baseUri The base URI. 71 * @param referenceUri The reference URI to resolve. 72 */ resolveToUri(@ullable String baseUri, @Nullable String referenceUri)73 public static Uri resolveToUri(@Nullable String baseUri, @Nullable String referenceUri) { 74 return Uri.parse(resolve(baseUri, referenceUri)); 75 } 76 77 /** 78 * Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}. 79 * 80 * <p>The resolution is performed as specified by RFC-3986. 81 * 82 * @param baseUri The base URI. 83 * @param referenceUri The reference URI to resolve. 84 */ resolve(@ullable String baseUri, @Nullable String referenceUri)85 public static String resolve(@Nullable String baseUri, @Nullable String referenceUri) { 86 StringBuilder uri = new StringBuilder(); 87 88 // Map null onto empty string, to make the following logic simpler. 89 baseUri = baseUri == null ? "" : baseUri; 90 referenceUri = referenceUri == null ? "" : referenceUri; 91 92 int[] refIndices = getUriIndices(referenceUri); 93 if (refIndices[SCHEME_COLON] != -1) { 94 // The reference is absolute. The target Uri is the reference. 95 uri.append(referenceUri); 96 removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]); 97 return uri.toString(); 98 } 99 100 int[] baseIndices = getUriIndices(baseUri); 101 if (refIndices[FRAGMENT] == 0) { 102 // The reference is empty or contains just the fragment part, then the target Uri is the 103 // concatenation of the base Uri without its fragment, and the reference. 104 return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString(); 105 } 106 107 if (refIndices[QUERY] == 0) { 108 // The reference starts with the query part. The target is the base up to (but excluding) the 109 // query, plus the reference. 110 return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString(); 111 } 112 113 if (refIndices[PATH] != 0) { 114 // The reference has authority. The target is the base scheme plus the reference. 115 int baseLimit = baseIndices[SCHEME_COLON] + 1; 116 uri.append(baseUri, 0, baseLimit).append(referenceUri); 117 return removeDotSegments(uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY]); 118 } 119 120 if (referenceUri.charAt(refIndices[PATH]) == '/') { 121 // The reference path is rooted. The target is the base scheme and authority (if any), plus 122 // the reference. 123 uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri); 124 return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY]); 125 } 126 127 // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment, 128 // and the reference. This can be split into 2 cases: 129 if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH] 130 && baseIndices[PATH] == baseIndices[QUERY]) { 131 // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is 132 // needed after the authority, before appending the reference. 133 uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri); 134 return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1); 135 } else { 136 // Case 2: Otherwise, find the last '/' in the base hier-part and append the reference after 137 // it. If base hier-part has no '/', it could only mean that it is completely empty or 138 // contains only one segment, in which case the whole hier-part is excluded and the reference 139 // is appended right after the base scheme colon without an added '/'. 140 int lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1); 141 int baseLimit = lastSlashIndex == -1 ? baseIndices[PATH] : lastSlashIndex + 1; 142 uri.append(baseUri, 0, baseLimit).append(referenceUri); 143 return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]); 144 } 145 } 146 147 /** 148 * Removes query parameter from an Uri, if present. 149 * 150 * @param uri The uri. 151 * @param queryParameterName The name of the query parameter. 152 * @return The uri without the query parameter. 153 */ removeQueryParameter(Uri uri, String queryParameterName)154 public static Uri removeQueryParameter(Uri uri, String queryParameterName) { 155 Uri.Builder builder = uri.buildUpon(); 156 builder.clearQuery(); 157 for (String key : uri.getQueryParameterNames()) { 158 if (!key.equals(queryParameterName)) { 159 for (String value : uri.getQueryParameters(key)) { 160 builder.appendQueryParameter(key, value); 161 } 162 } 163 } 164 return builder.build(); 165 } 166 167 /** 168 * Removes dot segments from the path of a URI. 169 * 170 * @param uri A {@link StringBuilder} containing the URI. 171 * @param offset The index of the start of the path in {@code uri}. 172 * @param limit The limit (exclusive) of the path in {@code uri}. 173 */ removeDotSegments(StringBuilder uri, int offset, int limit)174 private static String removeDotSegments(StringBuilder uri, int offset, int limit) { 175 if (offset >= limit) { 176 // Nothing to do. 177 return uri.toString(); 178 } 179 if (uri.charAt(offset) == '/') { 180 // If the path starts with a /, always retain it. 181 offset++; 182 } 183 // The first character of the current path segment. 184 int segmentStart = offset; 185 int i = offset; 186 while (i <= limit) { 187 int nextSegmentStart; 188 if (i == limit) { 189 nextSegmentStart = i; 190 } else if (uri.charAt(i) == '/') { 191 nextSegmentStart = i + 1; 192 } else { 193 i++; 194 continue; 195 } 196 // We've encountered the end of a segment or the end of the path. If the final segment was 197 // "." or "..", remove the appropriate segments of the path. 198 if (i == segmentStart + 1 && uri.charAt(segmentStart) == '.') { 199 // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi". 200 uri.delete(segmentStart, nextSegmentStart); 201 limit -= nextSegmentStart - segmentStart; 202 i = segmentStart; 203 } else if (i == segmentStart + 2 && uri.charAt(segmentStart) == '.' 204 && uri.charAt(segmentStart + 1) == '.') { 205 // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi". 206 int prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1; 207 int removeFrom = prevSegmentStart > offset ? prevSegmentStart : offset; 208 uri.delete(removeFrom, nextSegmentStart); 209 limit -= nextSegmentStart - removeFrom; 210 segmentStart = prevSegmentStart; 211 i = prevSegmentStart; 212 } else { 213 i++; 214 segmentStart = i; 215 } 216 } 217 return uri.toString(); 218 } 219 220 /** 221 * Calculates indices of the constituent components of a URI. 222 * 223 * @param uriString The URI as a string. 224 * @return The corresponding indices. 225 */ getUriIndices(String uriString)226 private static int[] getUriIndices(String uriString) { 227 int[] indices = new int[INDEX_COUNT]; 228 if (TextUtils.isEmpty(uriString)) { 229 indices[SCHEME_COLON] = -1; 230 return indices; 231 } 232 233 // Determine outer structure from right to left. 234 // Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ] 235 int length = uriString.length(); 236 int fragmentIndex = uriString.indexOf('#'); 237 if (fragmentIndex == -1) { 238 fragmentIndex = length; 239 } 240 int queryIndex = uriString.indexOf('?'); 241 if (queryIndex == -1 || queryIndex > fragmentIndex) { 242 // '#' before '?': '?' is within the fragment. 243 queryIndex = fragmentIndex; 244 } 245 // Slashes are allowed only in hier-part so any colon after the first slash is part of the 246 // hier-part, not the scheme colon separator. 247 int schemeIndexLimit = uriString.indexOf('/'); 248 if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) { 249 schemeIndexLimit = queryIndex; 250 } 251 int schemeIndex = uriString.indexOf(':'); 252 if (schemeIndex > schemeIndexLimit) { 253 // '/' before ':' 254 schemeIndex = -1; 255 } 256 257 // Determine hier-part structure: hier-part = "//" authority path / path 258 // This block can also cope with schemeIndex == -1. 259 boolean hasAuthority = schemeIndex + 2 < queryIndex 260 && uriString.charAt(schemeIndex + 1) == '/' 261 && uriString.charAt(schemeIndex + 2) == '/'; 262 int pathIndex; 263 if (hasAuthority) { 264 pathIndex = uriString.indexOf('/', schemeIndex + 3); // find first '/' after "://" 265 if (pathIndex == -1 || pathIndex > queryIndex) { 266 pathIndex = queryIndex; 267 } 268 } else { 269 pathIndex = schemeIndex + 1; 270 } 271 272 indices[SCHEME_COLON] = schemeIndex; 273 indices[PATH] = pathIndex; 274 indices[QUERY] = queryIndex; 275 indices[FRAGMENT] = fragmentIndex; 276 return indices; 277 } 278 279 } 280