• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2019 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.url;
6 
7 import android.os.SystemClock;
8 import android.text.TextUtils;
9 
10 import androidx.annotation.Nullable;
11 
12 import com.google.errorprone.annotations.DoNotMock;
13 
14 import org.jni_zero.CalledByNative;
15 import org.jni_zero.JNINamespace;
16 import org.jni_zero.NativeMethods;
17 
18 import org.chromium.base.Log;
19 import org.chromium.base.ThreadUtils;
20 import org.chromium.base.library_loader.LibraryLoader;
21 import org.chromium.base.metrics.RecordHistogram;
22 import org.chromium.base.task.PostTask;
23 import org.chromium.base.task.TaskTraits;
24 import org.chromium.url.mojom.Url;
25 import org.chromium.url.mojom.UrlConstants;
26 
27 import java.util.Random;
28 
29 /**
30  * An immutable Java wrapper for GURL, Chromium's URL parsing library.
31  *
32  * This class is safe to use during startup, but will block on the native library being sufficiently
33  * loaded to use native GURL (and will not wait for content initialization). In practice it's very
34  * unlikely that this will actually block startup unless used extremely early, in which case you
35  * should probably seek an alternative solution to using GURL.
36  *
37  * The design of this class avoids destruction/finalization by caching all values necessary to
38  * reconstruct a GURL in Java, allowing it to be much faster in the common case and easier to use.
39  */
40 @JNINamespace("url")
41 @DoNotMock("Create a real instance instead.")
42 public class GURL {
43     private static final String TAG = "GURL";
44     /* package */ static final int SERIALIZER_VERSION = 1;
45     /* package */ static final char SERIALIZER_DELIMITER = '\0';
46 
47     @FunctionalInterface
48     public interface ReportDebugThrowableCallback {
run(Throwable throwable)49         void run(Throwable throwable);
50     }
51 
52     /**
53      * Exception signalling that a GURL failed to parse due to an unexpected version marker in the
54      * serialized input.
55      */
56     public static class BadSerializerVersionException extends RuntimeException {}
57 
58     // Right now this is only collecting reports on Canary which has a relatively small population.
59     private static final int DEBUG_REPORT_PERCENTAGE = 10;
60     private static ReportDebugThrowableCallback sReportCallback;
61 
62     // TODO(https://crbug.com/1039841): Right now we return a new String with each request for a
63     //      GURL component other than the spec itself. Should we cache return Strings (as
64     //      WeakReference?) so that callers can share String memory?
65     private String mSpec;
66     private boolean mIsValid;
67     private Parsed mParsed;
68 
69     private static class Holder {
70         private static GURL sEmptyGURL = new GURL("");
71     }
72 
73     @CalledByNative
emptyGURL()74     public static GURL emptyGURL() {
75         return Holder.sEmptyGURL;
76     }
77 
78     /**
79      * Create a new GURL.
80      *
81      * @param uri The string URI representation to parse into a GURL.
82      */
GURL(String uri)83     public GURL(String uri) {
84         // Avoid a jni hop (and initializing the native library) for empty GURLs.
85         if (TextUtils.isEmpty(uri)) {
86             mSpec = "";
87             mParsed = Parsed.createEmpty();
88             return;
89         }
90         ensureNativeInitializedForGURL();
91         getNatives().init(uri, this);
92     }
93 
94     @CalledByNative
GURL()95     protected GURL() {}
96 
97     /** Enables debug stack trace gathering for GURL. */
setReportDebugThrowableCallback(ReportDebugThrowableCallback callback)98     public static void setReportDebugThrowableCallback(ReportDebugThrowableCallback callback) {
99         sReportCallback = callback;
100     }
101 
102     /**
103      * Ensures that the native library is sufficiently loaded for GURL usage.
104      *
105      * This function is public so that GURL-related usage like the UrlFormatter also counts towards
106      * the "Startup.Android.GURLEnsureMainDexInitialized" histogram.
107      */
ensureNativeInitializedForGURL()108     public static void ensureNativeInitializedForGURL() {
109         if (LibraryLoader.getInstance().isInitialized()) return;
110         long time = SystemClock.elapsedRealtime();
111         LibraryLoader.getInstance().ensureMainDexInitialized();
112         // Record metrics only for the UI thread where the delay in loading the library is relevant.
113         if (ThreadUtils.runningOnUiThread()) {
114             // "MainDex" in name of histogram is a dated reference to when we used to have 2
115             // sections of the native library, main dex and non-main dex. Maintaining name for
116             // consistency in metrics.
117             RecordHistogram.recordTimesHistogram(
118                     "Startup.Android.GURLEnsureMainDexInitialized",
119                     SystemClock.elapsedRealtime() - time);
120             if (sReportCallback != null && new Random().nextInt(100) < DEBUG_REPORT_PERCENTAGE) {
121                 final Throwable throwable =
122                         new Throwable("This is not a crash, please ignore. See crbug.com/1065377.");
123                 // This isn't an assert, because by design this is possible, but we would prefer
124                 // this path does not get hit more than necessary and getting stack traces from the
125                 // wild will help find issues.
126                 PostTask.postTask(
127                         TaskTraits.BEST_EFFORT_MAY_BLOCK,
128                         () -> {
129                             sReportCallback.run(throwable);
130                         });
131             }
132         }
133     }
134 
135     /** @return true if the GURL is null, empty, or invalid. */
isEmptyOrInvalid(@ullable GURL gurl)136     public static boolean isEmptyOrInvalid(@Nullable GURL gurl) {
137         return gurl == null || gurl.isEmpty() || !gurl.isValid();
138     }
139 
140     @CalledByNative
init(String spec, boolean isValid, Parsed parsed)141     private void init(String spec, boolean isValid, Parsed parsed) {
142         mSpec = spec;
143         mIsValid = isValid;
144         mParsed = parsed;
145     }
146 
147     @CalledByNative
toNativeGURL()148     private long toNativeGURL() {
149         return getNatives().createNative(mSpec, mIsValid, mParsed.toNativeParsed());
150     }
151 
152     /** See native GURL::is_valid(). */
isValid()153     public boolean isValid() {
154         return mIsValid;
155     }
156 
157     /** See native GURL::spec(). */
getSpec()158     public String getSpec() {
159         if (isValid() || mSpec.isEmpty()) return mSpec;
160         assert false : "Trying to get the spec of an invalid URL!";
161         return "";
162     }
163 
164     /**
165      * @return Either a valid Spec (see {@link #getSpec}), or an empty string.
166      */
getValidSpecOrEmpty()167     public String getValidSpecOrEmpty() {
168         if (isValid()) return mSpec;
169         return "";
170     }
171 
172     /** See native GURL::possibly_invalid_spec(). */
getPossiblyInvalidSpec()173     public String getPossiblyInvalidSpec() {
174         return mSpec;
175     }
176 
getComponent(int begin, int length)177     private String getComponent(int begin, int length) {
178         if (length <= 0) return "";
179         return mSpec.substring(begin, begin + length);
180     }
181 
182     /** See native GURL::scheme(). */
getScheme()183     public String getScheme() {
184         return getComponent(mParsed.mSchemeBegin, mParsed.mSchemeLength);
185     }
186 
187     /** See native GURL::username(). */
getUsername()188     public String getUsername() {
189         return getComponent(mParsed.mUsernameBegin, mParsed.mUsernameLength);
190     }
191 
192     /** See native GURL::password(). */
getPassword()193     public String getPassword() {
194         return getComponent(mParsed.mPasswordBegin, mParsed.mPasswordLength);
195     }
196 
197     /** See native GURL::host(). */
getHost()198     public String getHost() {
199         return getComponent(mParsed.mHostBegin, mParsed.mHostLength);
200     }
201 
202     /**
203      * See native GURL::port().
204      *
205      * Note: Do not convert this to an integer yourself. See native GURL::IntPort().
206      */
getPort()207     public String getPort() {
208         return getComponent(mParsed.mPortBegin, mParsed.mPortLength);
209     }
210 
211     /** See native GURL::path(). */
getPath()212     public String getPath() {
213         return getComponent(mParsed.mPathBegin, mParsed.mPathLength);
214     }
215 
216     /** See native GURL::query(). */
getQuery()217     public String getQuery() {
218         return getComponent(mParsed.mQueryBegin, mParsed.mQueryLength);
219     }
220 
221     /** See native GURL::ref(). */
getRef()222     public String getRef() {
223         return getComponent(mParsed.mRefBegin, mParsed.mRefLength);
224     }
225 
226     /**
227      * @return Whether the GURL is the empty String.
228      */
isEmpty()229     public boolean isEmpty() {
230         return mSpec.isEmpty();
231     }
232 
233     /** See native GURL::GetOrigin(). */
getOrigin()234     public GURL getOrigin() {
235         GURL target = new GURL();
236         getOriginInternal(target);
237         return target;
238     }
239 
getOriginInternal(GURL target)240     protected void getOriginInternal(GURL target) {
241         getNatives().getOrigin(mSpec, mIsValid, mParsed.toNativeParsed(), target);
242     }
243 
244     /** See native GURL::DomainIs(). */
domainIs(String domain)245     public boolean domainIs(String domain) {
246         return getNatives().domainIs(mSpec, mIsValid, mParsed.toNativeParsed(), domain);
247     }
248 
249     /**
250      * Returns a copy of the URL with components replaced. See native GURL::ReplaceComponents().
251      *
252      * <p>Rules for replacement: 1. If a `clear*` boolean param is true, the component will be
253      * removed from the result. 2. Otherwise if the corresponding string param is non-null, its
254      * value will be used to replace the component. 3. If the string is null and the `clear*`
255      * boolean is false, the component will not be modified.
256      *
257      * @param username Username replacement.
258      * @param clearUsername True if the result should not contain a username.
259      * @param password Password replacement.
260      * @param clearPassword True if the result should not contain a password.
261      * @return Copy of the URL with replacements applied.
262      */
replaceComponents( String username, boolean clearUsername, String password, boolean clearPassword)263     public GURL replaceComponents(
264             String username, boolean clearUsername, String password, boolean clearPassword) {
265         GURL result = new GURL();
266         getNatives()
267                 .replaceComponents(
268                         mSpec,
269                         mIsValid,
270                         mParsed.toNativeParsed(),
271                         username,
272                         clearUsername,
273                         password,
274                         clearPassword,
275                         result);
276         return result;
277     }
278 
279     @Override
hashCode()280     public final int hashCode() {
281         return mSpec.hashCode();
282     }
283 
284     @Override
equals(Object other)285     public final boolean equals(Object other) {
286         if (other == this) return true;
287         if (!(other instanceof GURL)) return false;
288         return mSpec.equals(((GURL) other).mSpec);
289     }
290 
291     /**
292      * Serialize a GURL to a String, to be used with {@link GURL#deserialize(String)}.
293      *
294      * Note that a serialized GURL should only be used internally to Chrome, and should *never* be
295      * used if coming from an untrusted source.
296      *
297      * @return A serialzed GURL.
298      */
serialize()299     public final String serialize() {
300         StringBuilder builder = new StringBuilder();
301         builder.append(SERIALIZER_VERSION).append(SERIALIZER_DELIMITER);
302         builder.append(mIsValid).append(SERIALIZER_DELIMITER);
303         builder.append(mParsed.serialize()).append(SERIALIZER_DELIMITER);
304         builder.append(mSpec);
305         String serialization = builder.toString();
306         return Integer.toString(serialization.length()) + SERIALIZER_DELIMITER + serialization;
307     }
308 
309     /**
310      * Deserialize a GURL serialized with {@link GURL#serialize()}. This will re-parse in case of
311      * version mismatch, which may trigger undesired native loading. {@see
312      * deserializeLatestVersionOnly} if you want to fail in case of version mismatch.
313      *
314      * This function should *never* be used on a String coming from an untrusted source.
315      *
316      * @return The deserialized GURL (or null if the input is empty).
317      */
deserialize(@ullable String gurl)318     public static GURL deserialize(@Nullable String gurl) {
319         try {
320             return deserializeLatestVersionOnly(gurl);
321         } catch (BadSerializerVersionException be) {
322             // Just re-parse the GURL on version changes.
323             String[] tokens = gurl.split(Character.toString(SERIALIZER_DELIMITER));
324             return new GURL(getSpecFromTokens(gurl, tokens));
325         } catch (Exception e) {
326             // This is unexpected, maybe the storage got corrupted somehow?
327             Log.w(TAG, "Exception while deserializing a GURL: " + gurl, e);
328             return emptyGURL();
329         }
330     }
331 
332     /**
333      * Deserialize a GURL serialized with {@link #serialize()}, throwing {@code
334      * BadSerializerException} if the serialized input has a version other than the latest. This
335      * function should never be used on a String coming from an untrusted source.
336      */
deserializeLatestVersionOnly(@ullable String gurl)337     public static GURL deserializeLatestVersionOnly(@Nullable String gurl) {
338         if (TextUtils.isEmpty(gurl)) return emptyGURL();
339         String[] tokens = gurl.split(Character.toString(SERIALIZER_DELIMITER));
340 
341         // First token MUST always be the length of the serialized data.
342         String length = tokens[0];
343         if (gurl.length() != Integer.parseInt(length) + length.length() + 1) {
344             throw new IllegalArgumentException("Serialized GURL had the wrong length.");
345         }
346 
347         String spec = getSpecFromTokens(gurl, tokens);
348         // Second token MUST always be the version number.
349         int version = Integer.parseInt(tokens[1]);
350         if (version != SERIALIZER_VERSION) {
351             throw new BadSerializerVersionException();
352         }
353 
354         boolean isValid = Boolean.parseBoolean(tokens[2]);
355         Parsed parsed = Parsed.deserialize(tokens, 3);
356         GURL result = new GURL();
357         result.init(spec, isValid, parsed);
358         return result;
359     }
360 
getSpecFromTokens(String gurl, String[] tokens)361     private static String getSpecFromTokens(String gurl, String[] tokens) {
362         // Last token MUST always be the original spec.
363         // Special case for empty spec - it won't get its own token.
364         return gurl.endsWith(Character.toString(SERIALIZER_DELIMITER))
365                 ? ""
366                 : tokens[tokens.length - 1];
367     }
368 
369     /**
370      * Returns the instance of {@link Natives}. The Robolectric Shadow intercepts invocations of
371      * this method.
372      *
373      * <p>Unlike {@code GURLJni.TEST_HOOKS.setInstanceForTesting}, shadowing this method doesn't
374      * rely on tests correctly cleaning up global state.
375      */
getNatives()376     private static Natives getNatives() {
377         return GURLJni.get();
378     }
379 
380     /** Inits this GURL with the internal state of another GURL. */
initForTesting(GURL gurl)381     /* package */ void initForTesting(GURL gurl) {
382         init(gurl.mSpec, gurl.mIsValid, gurl.mParsed);
383     }
384 
385     /** @return A Mojom representation of this URL. */
toMojom()386     public Url toMojom() {
387         Url url = new Url();
388         // See url/mojom/url_gurl_mojom_traits.cc.
389         url.url =
390                 TextUtils.isEmpty(getPossiblyInvalidSpec())
391                                 || getPossiblyInvalidSpec().length() > UrlConstants.MAX_URL_CHARS
392                                 || !isValid()
393                         ? ""
394                         : getPossiblyInvalidSpec();
395         return url;
396     }
397 
398     @NativeMethods
399     interface Natives {
400         /** Initializes the provided |target| by parsing the provided |uri|. */
init(String uri, GURL target)401         void init(String uri, GURL target);
402 
403         /**
404          * Reconstructs the native GURL for this Java GURL and initializes |target| with its Origin.
405          */
getOrigin(String spec, boolean isValid, long nativeParsed, GURL target)406         void getOrigin(String spec, boolean isValid, long nativeParsed, GURL target);
407 
408         /** Reconstructs the native GURL for this Java GURL, and calls GURL.DomainIs. */
domainIs(String spec, boolean isValid, long nativeParsed, String domain)409         boolean domainIs(String spec, boolean isValid, long nativeParsed, String domain);
410 
411         /** Reconstructs the native GURL for this Java GURL, returning its native pointer. */
createNative(String spec, boolean isValid, long nativeParsed)412         long createNative(String spec, boolean isValid, long nativeParsed);
413 
414         /**
415          * Reconstructs the native GURL for this Java GURL and initializes |result| with the result
416          * of ReplaceComponents.
417          */
replaceComponents( String spec, boolean isValid, long nativeParsed, String username, boolean clearUsername, String password, boolean clearPassword, GURL result)418         void replaceComponents(
419                 String spec,
420                 boolean isValid,
421                 long nativeParsed,
422                 String username,
423                 boolean clearUsername,
424                 String password,
425                 boolean clearPassword,
426                 GURL result);
427     }
428 }
429