blob: 75f289d05e49d9d3897201772957aaba8bc1516c [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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 org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.annotations.NativeMethods;
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 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")
@MainDex
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);
}
// 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();
GURLJni.get().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()) {
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;
// Ensure that the spec only contains US-ASCII or the parsed indices will be wrong.
assert mSpec.matches("\\A\\p{ASCII}*\\z");
mIsValid = isValid;
mParsed = parsed;
}
@CalledByNative
private long toNativeGURL() {
return GURLJni.get().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) {
GURLJni.get().getOrigin(mSpec, mIsValid, mParsed.toNativeParsed(), target);
}
/**
* See native GURL::DomainIs().
*/
public boolean domainIs(String domain) {
return GURLJni.get().domainIs(mSpec, mIsValid, mParsed.toNativeParsed(), domain);
}
@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 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 {
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.");
}
// Last token MUST always be the original spec - just re-parse the GURL on version
// changes.
String spec = tokens[tokens.length - 1];
// Special case for empty spec - it won't get its own token.
if (gurl.endsWith(Character.toString(SERIALIZER_DELIMITER))) spec = "";
// Second token MUST always be the version number.
int version = Integer.parseInt(tokens[1]);
if (version != SERIALIZER_VERSION) return new GURL(spec);
boolean isValid = Boolean.parseBoolean(tokens[2]);
Parsed parsed = Parsed.deserialize(tokens, 3);
GURL result = new GURL();
result.init(spec, isValid, parsed);
return result;
} catch (Exception e) {
// This is unexpected, maybe the storage got corrupted somehow?
Log.w(TAG, "Exception while deserializing a GURL: " + gurl, e);
return emptyGURL();
}
}
@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);
}
}