blob: 2d9de18b628dea4d2cbb1d4e18eaa76ee9aadae6 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.url;
import android.os.SystemClock;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.errorprone.annotations.DoNotMock;
import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.url.mojom.Url;
import org.chromium.url.mojom.UrlConstants;
import java.util.Random;
/**
* An immutable Java wrapper for GURL, Chromium's URL parsing library.
*
* This class is safe to use during startup, but will block on the native library being sufficiently
* loaded to use native GURL (and will not wait for content initialization). In practice it's very
* unlikely that this will actually block startup unless used extremely early, in which case you
* should probably seek an alternative solution to using GURL.
*
* The design of this class avoids destruction/finalization by caching all values necessary to
* reconstruct a GURL in Java, allowing it to be much faster in the common case and easier to use.
*/
@JNINamespace("url")
@DoNotMock("Create a real instance instead.")
public class GURL {
private static final String TAG = "GURL";
/* package */ static final int SERIALIZER_VERSION = 1;
/* package */ static final char SERIALIZER_DELIMITER = '\0';
@FunctionalInterface
public interface ReportDebugThrowableCallback {
void run(Throwable throwable);
}
/**
* Exception signalling that a GURL failed to parse due to an unexpected version marker in the
* serialized input.
*/
public static class BadSerializerVersionException extends RuntimeException {}
// Right now this is only collecting reports on Canary which has a relatively small population.
private static final int DEBUG_REPORT_PERCENTAGE = 10;
private static ReportDebugThrowableCallback sReportCallback;
// TODO(https://crbug.com/1039841): Right now we return a new String with each request for a
// GURL component other than the spec itself. Should we cache return Strings (as
// WeakReference?) so that callers can share String memory?
private String mSpec;
private boolean mIsValid;
private Parsed mParsed;
private static class Holder {
private static GURL sEmptyGURL = new GURL("");
}
@CalledByNative
public static GURL emptyGURL() {
return Holder.sEmptyGURL;
}
/**
* Create a new GURL.
*
* @param uri The string URI representation to parse into a GURL.
*/
public GURL(String uri) {
// Avoid a jni hop (and initializing the native library) for empty GURLs.
if (TextUtils.isEmpty(uri)) {
mSpec = "";
mParsed = Parsed.createEmpty();
return;
}
ensureNativeInitializedForGURL();
getNatives().init(uri, this);
}
@CalledByNative
protected GURL() {}
/** Enables debug stack trace gathering for GURL. */
public static void setReportDebugThrowableCallback(ReportDebugThrowableCallback callback) {
sReportCallback = callback;
}
/**
* Ensures that the native library is sufficiently loaded for GURL usage.
*
* This function is public so that GURL-related usage like the UrlFormatter also counts towards
* the "Startup.Android.GURLEnsureMainDexInitialized" histogram.
*/
public static void ensureNativeInitializedForGURL() {
if (LibraryLoader.getInstance().isInitialized()) return;
long time = SystemClock.elapsedRealtime();
LibraryLoader.getInstance().ensureMainDexInitialized();
// Record metrics only for the UI thread where the delay in loading the library is relevant.
if (ThreadUtils.runningOnUiThread()) {
// "MainDex" in name of histogram is a dated reference to when we used to have 2
// sections of the native library, main dex and non-main dex. Maintaining name for
// consistency in metrics.
RecordHistogram.recordTimesHistogram(
"Startup.Android.GURLEnsureMainDexInitialized",
SystemClock.elapsedRealtime() - time);
if (sReportCallback != null && new Random().nextInt(100) < DEBUG_REPORT_PERCENTAGE) {
final Throwable throwable =
new Throwable("This is not a crash, please ignore. See crbug.com/1065377.");
// This isn't an assert, because by design this is possible, but we would prefer
// this path does not get hit more than necessary and getting stack traces from the
// wild will help find issues.
PostTask.postTask(
TaskTraits.BEST_EFFORT_MAY_BLOCK,
() -> {
sReportCallback.run(throwable);
});
}
}
}
/** @return true if the GURL is null, empty, or invalid. */
public static boolean isEmptyOrInvalid(@Nullable GURL gurl) {
return gurl == null || gurl.isEmpty() || !gurl.isValid();
}
@CalledByNative
private void init(String spec, boolean isValid, Parsed parsed) {
mSpec = spec;
mIsValid = isValid;
mParsed = parsed;
}
@CalledByNative
private long toNativeGURL() {
return getNatives().createNative(mSpec, mIsValid, mParsed.toNativeParsed());
}
/** See native GURL::is_valid(). */
public boolean isValid() {
return mIsValid;
}
/** See native GURL::spec(). */
public String getSpec() {
if (isValid() || mSpec.isEmpty()) return mSpec;
assert false : "Trying to get the spec of an invalid URL!";
return "";
}
/**
* @return Either a valid Spec (see {@link #getSpec}), or an empty string.
*/
public String getValidSpecOrEmpty() {
if (isValid()) return mSpec;
return "";
}
/** See native GURL::possibly_invalid_spec(). */
public String getPossiblyInvalidSpec() {
return mSpec;
}
private String getComponent(int begin, int length) {
if (length <= 0) return "";
return mSpec.substring(begin, begin + length);
}
/** See native GURL::scheme(). */
public String getScheme() {
return getComponent(mParsed.mSchemeBegin, mParsed.mSchemeLength);
}
/** See native GURL::username(). */
public String getUsername() {
return getComponent(mParsed.mUsernameBegin, mParsed.mUsernameLength);
}
/** See native GURL::password(). */
public String getPassword() {
return getComponent(mParsed.mPasswordBegin, mParsed.mPasswordLength);
}
/** See native GURL::host(). */
public String getHost() {
return getComponent(mParsed.mHostBegin, mParsed.mHostLength);
}
/**
* See native GURL::port().
*
* Note: Do not convert this to an integer yourself. See native GURL::IntPort().
*/
public String getPort() {
return getComponent(mParsed.mPortBegin, mParsed.mPortLength);
}
/** See native GURL::path(). */
public String getPath() {
return getComponent(mParsed.mPathBegin, mParsed.mPathLength);
}
/** See native GURL::query(). */
public String getQuery() {
return getComponent(mParsed.mQueryBegin, mParsed.mQueryLength);
}
/** See native GURL::ref(). */
public String getRef() {
return getComponent(mParsed.mRefBegin, mParsed.mRefLength);
}
/**
* @return Whether the GURL is the empty String.
*/
public boolean isEmpty() {
return mSpec.isEmpty();
}
/** See native GURL::GetOrigin(). */
public GURL getOrigin() {
GURL target = new GURL();
getOriginInternal(target);
return target;
}
protected void getOriginInternal(GURL target) {
getNatives().getOrigin(mSpec, mIsValid, mParsed.toNativeParsed(), target);
}
/** See native GURL::DomainIs(). */
public boolean domainIs(String domain) {
return getNatives().domainIs(mSpec, mIsValid, mParsed.toNativeParsed(), domain);
}
/**
* Returns a copy of the URL with components replaced. See native GURL::ReplaceComponents().
*
* <p>Rules for replacement: 1. If a `clear*` boolean param is true, the component will be
* removed from the result. 2. Otherwise if the corresponding string param is non-null, its
* value will be used to replace the component. 3. If the string is null and the `clear*`
* boolean is false, the component will not be modified.
*
* @param username Username replacement.
* @param clearUsername True if the result should not contain a username.
* @param password Password replacement.
* @param clearPassword True if the result should not contain a password.
* @return Copy of the URL with replacements applied.
*/
public GURL replaceComponents(
String username, boolean clearUsername, String password, boolean clearPassword) {
GURL result = new GURL();
getNatives()
.replaceComponents(
mSpec,
mIsValid,
mParsed.toNativeParsed(),
username,
clearUsername,
password,
clearPassword,
result);
return result;
}
@Override
public final int hashCode() {
return mSpec.hashCode();
}
@Override
public final boolean equals(Object other) {
if (other == this) return true;
if (!(other instanceof GURL)) return false;
return mSpec.equals(((GURL) other).mSpec);
}
/**
* Serialize a GURL to a String, to be used with {@link GURL#deserialize(String)}.
*
* Note that a serialized GURL should only be used internally to Chrome, and should *never* be
* used if coming from an untrusted source.
*
* @return A serialzed GURL.
*/
public final String serialize() {
StringBuilder builder = new StringBuilder();
builder.append(SERIALIZER_VERSION).append(SERIALIZER_DELIMITER);
builder.append(mIsValid).append(SERIALIZER_DELIMITER);
builder.append(mParsed.serialize()).append(SERIALIZER_DELIMITER);
builder.append(mSpec);
String serialization = builder.toString();
return Integer.toString(serialization.length()) + SERIALIZER_DELIMITER + serialization;
}
/**
* Deserialize a GURL serialized with {@link GURL#serialize()}. This will re-parse in case of
* version mismatch, which may trigger undesired native loading. {@see
* deserializeLatestVersionOnly} if you want to fail in case of version mismatch.
*
* This function should *never* be used on a String coming from an untrusted source.
*
* @return The deserialized GURL (or null if the input is empty).
*/
public static GURL deserialize(@Nullable String gurl) {
try {
return deserializeLatestVersionOnly(gurl);
} catch (BadSerializerVersionException be) {
// Just re-parse the GURL on version changes.
String[] tokens = gurl.split(Character.toString(SERIALIZER_DELIMITER));
return new GURL(getSpecFromTokens(gurl, tokens));
} catch (Exception e) {
// This is unexpected, maybe the storage got corrupted somehow?
Log.w(TAG, "Exception while deserializing a GURL: " + gurl, e);
return emptyGURL();
}
}
/**
* Deserialize a GURL serialized with {@link #serialize()}, throwing {@code
* BadSerializerException} if the serialized input has a version other than the latest. This
* function should never be used on a String coming from an untrusted source.
*/
public static GURL deserializeLatestVersionOnly(@Nullable String gurl) {
if (TextUtils.isEmpty(gurl)) return emptyGURL();
String[] tokens = gurl.split(Character.toString(SERIALIZER_DELIMITER));
// First token MUST always be the length of the serialized data.
String length = tokens[0];
if (gurl.length() != Integer.parseInt(length) + length.length() + 1) {
throw new IllegalArgumentException("Serialized GURL had the wrong length.");
}
String spec = getSpecFromTokens(gurl, tokens);
// Second token MUST always be the version number.
int version = Integer.parseInt(tokens[1]);
if (version != SERIALIZER_VERSION) {
throw new BadSerializerVersionException();
}
boolean isValid = Boolean.parseBoolean(tokens[2]);
Parsed parsed = Parsed.deserialize(tokens, 3);
GURL result = new GURL();
result.init(spec, isValid, parsed);
return result;
}
private static String getSpecFromTokens(String gurl, String[] tokens) {
// Last token MUST always be the original spec.
// Special case for empty spec - it won't get its own token.
return gurl.endsWith(Character.toString(SERIALIZER_DELIMITER))
? ""
: tokens[tokens.length - 1];
}
/**
* Returns the instance of {@link Natives}. The Robolectric Shadow intercepts invocations of
* this method.
*
* <p>Unlike {@code GURLJni.TEST_HOOKS.setInstanceForTesting}, shadowing this method doesn't
* rely on tests correctly cleaning up global state.
*/
private static Natives getNatives() {
return GURLJni.get();
}
/** Inits this GURL with the internal state of another GURL. */
/* package */ void initForTesting(GURL gurl) {
init(gurl.mSpec, gurl.mIsValid, gurl.mParsed);
}
/** @return A Mojom representation of this URL. */
public Url toMojom() {
Url url = new Url();
// See url/mojom/url_gurl_mojom_traits.cc.
url.url =
TextUtils.isEmpty(getPossiblyInvalidSpec())
|| getPossiblyInvalidSpec().length() > UrlConstants.MAX_URL_CHARS
|| !isValid()
? ""
: getPossiblyInvalidSpec();
return url;
}
@NativeMethods
interface Natives {
/** Initializes the provided |target| by parsing the provided |uri|. */
void init(String uri, GURL target);
/**
* Reconstructs the native GURL for this Java GURL and initializes |target| with its Origin.
*/
void getOrigin(String spec, boolean isValid, long nativeParsed, GURL target);
/** Reconstructs the native GURL for this Java GURL, and calls GURL.DomainIs. */
boolean domainIs(String spec, boolean isValid, long nativeParsed, String domain);
/** Reconstructs the native GURL for this Java GURL, returning its native pointer. */
long createNative(String spec, boolean isValid, long nativeParsed);
/**
* Reconstructs the native GURL for this Java GURL and initializes |result| with the result
* of ReplaceComponents.
*/
void replaceComponents(
String spec,
boolean isValid,
long nativeParsed,
String username,
boolean clearUsername,
String password,
boolean clearPassword,
GURL result);
}
}