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