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