• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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