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