Add a core-util-jvm module.

This is basically a location where we can put common utils that can also
be imported by libsignal-service (which is java-only, no android
dependency).
This commit is contained in:
Greyson Parrelli
2023-10-04 22:21:20 -04:00
committed by Nicholas Tinsley
parent 6be1413d7d
commit 7d5786ea93
115 changed files with 323 additions and 591 deletions

View File

@@ -1,78 +0,0 @@
package org.signal.core.util;
import java.util.Locale;
/**
* A set of utilities to make working with Bitmasks easier.
*/
public final class Bitmask {
/**
* Reads a bitmasked boolean from a long at the requested position.
*/
public static boolean read(long value, int position) {
return read(value, position, 1) > 0;
}
/**
* Reads a bitmasked value from a long at the requested position.
*
* @param value The value your are reading state from
* @param position The position you'd like to read from
* @param flagBitSize How many bits are in each flag
* @return The value at the requested position
*/
public static long read(long value, int position, int flagBitSize) {
checkArgument(flagBitSize >= 0, "Must have a positive bit size! size: " + flagBitSize);
int bitsToShift = position * flagBitSize;
checkArgument(bitsToShift + flagBitSize <= 64 && position >= 0, String.format(Locale.US, "Your position is out of bounds! position: %d, flagBitSize: %d", position, flagBitSize));
long shifted = value >>> bitsToShift;
long mask = twoToThe(flagBitSize) - 1;
return shifted & mask;
}
/**
* Sets the value at the specified position in a single-bit bitmasked long.
*/
public static long update(long existing, int position, boolean value) {
return update(existing, position, 1, value ? 1 : 0);
}
/**
* Updates the value in a bitmasked long.
*
* @param existing The existing state of the bitmask
* @param position The position you'd like to update
* @param flagBitSize How many bits are in each flag
* @param value The value you'd like to set at the specified position
* @return The updated bitmask
*/
public static long update(long existing, int position, int flagBitSize, long value) {
checkArgument(flagBitSize >= 0, "Must have a positive bit size! size: " + flagBitSize);
checkArgument(value >= 0, "Value must be positive! value: " + value);
checkArgument(value < twoToThe(flagBitSize), String.format(Locale.US, "Value is larger than you can hold for the given bitsize! value: %d, flagBitSize: %d", value, flagBitSize));
int bitsToShift = position * flagBitSize;
checkArgument(bitsToShift + flagBitSize <= 64 && position >= 0, String.format(Locale.US, "Your position is out of bounds! position: %d, flagBitSize: %d", position, flagBitSize));
long clearMask = ~((twoToThe(flagBitSize) - 1) << bitsToShift);
long cleared = existing & clearMask;
long shiftedValue = value << bitsToShift;
return cleared | shiftedValue;
}
/** Simple method to do 2^n. Giving it a name just so it's clear what's happening. */
private static long twoToThe(long n) {
return 1 << n;
}
private static void checkArgument(boolean state, String message) {
if (!state) {
throw new IllegalArgumentException(message);
}
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util
inline val Long.bytes: ByteSize
get() = ByteSize(this)
inline val Long.kibiBytes: ByteSize
get() = (this * 1024).bytes
inline val Long.mebiBytes: ByteSize
get() = (this * 1024).kibiBytes
inline val Long.gibiBytes: ByteSize
get() = (this * 1024).mebiBytes
class ByteSize(val bytes: Long) {
val inWholeBytes: Long
get() = bytes
val inWholeKibiBytes: Long
get() = bytes / 1024
val inWholeMebiBytes: Long
get() = inWholeKibiBytes / 1024
val inWholeGibiBytes: Long
get() = inWholeMebiBytes / 1024
val inKibiBytes: Float
get() = bytes / 1024f
val inMebiBytes: Float
get() = inKibiBytes / 1024f
val inGibiBytes: Float
get() = inMebiBytes / 1024f
}

View File

@@ -1,43 +0,0 @@
package org.signal.core.util;
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
public final class ColorUtil {
private ColorUtil() {}
public static int blendARGB(@ColorInt int color1,
@ColorInt int color2,
@FloatRange(from = 0.0, to = 1.0) float ratio)
{
final float inverseRatio = 1 - ratio;
float a = alpha(color1) * inverseRatio + alpha(color2) * ratio;
float r = red(color1) * inverseRatio + red(color2) * ratio;
float g = green(color1) * inverseRatio + green(color2) * ratio;
float b = blue(color1) * inverseRatio + blue(color2) * ratio;
return argb((int) a, (int) r, (int) g, (int) b);
}
private static int alpha(int color) {
return color >>> 24;
}
private static int red(int color) {
return (color >> 16) & 0xFF;
}
private static int green(int color) {
return (color >> 8) & 0xFF;
}
private static int blue(int color) {
return color & 0xFF;
}
private static int argb(int alpha, int red, int green, int blue) {
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}
}

View File

@@ -1,16 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util
/**
* Rounds a number to the specified number of places. e.g.
*
* 1.123456f.roundedString(2) = 1.12
* 1.123456f.roundedString(5) = 1.12346
*/
fun Double.roundedString(places: Int): String {
return String.format("%.${places}f", this)
}

View File

@@ -1,16 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util
/**
* Rounds a number to the specified number of places. e.g.
*
* 1.123456f.roundedString(2) = 1.12
* 1.123456f.roundedString(5) = 1.12346
*/
fun Float.roundedString(places: Int): String {
return String.format("%.${places}f", this)
}

View File

@@ -5,6 +5,7 @@ import android.text.Spanned;
import org.signal.core.util.logging.Log;
/**
* This filter will constrain edits not to make the number of character breaks of the text
* greater than the specified maximum.

View File

@@ -1,146 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.signal.core.util;
import java.io.IOException;
/**
* Utility for generating hex dumps.
*/
public class Hex {
private final static int HEX_DIGITS_START = 10;
private final static int ASCII_TEXT_START = HEX_DIGITS_START + (16*2 + (16/2));
final static String EOL = System.getProperty("line.separator");
private final static char[] HEX_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
public static String toString(byte[] bytes) {
return toString(bytes, 0, bytes.length);
}
public static String toString(byte[] bytes, int offset, int length) {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < length; i++) {
appendHexChar(buf, bytes[offset + i]);
buf.append(' ');
}
return buf.toString();
}
public static String toStringCondensed(byte[] bytes) {
StringBuffer buf = new StringBuffer();
for (int i=0;i<bytes.length;i++) {
appendHexChar(buf, bytes[i]);
}
return buf.toString();
}
public static byte[] fromStringCondensed(String encoded) throws IOException {
final char[] data = encoded.toCharArray();
final int len = data.length;
if ((len & 0x01) != 0) {
throw new IOException("Odd number of characters.");
}
final byte[] out = new byte[len >> 1];
// two characters form the hex value.
for (int i = 0, j = 0; j < len; i++) {
int f = Character.digit(data[j], 16) << 4;
j++;
f = f | Character.digit(data[j], 16);
j++;
out[i] = (byte) (f & 0xFF);
}
return out;
}
public static byte[] fromStringOrThrow(String encoded) {
try {
return fromStringCondensed(encoded);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static String dump(byte[] bytes) {
return dump(bytes, 0, bytes.length);
}
public static String dump(byte[] bytes, int offset, int length) {
StringBuffer buf = new StringBuffer();
int lines = ((length - 1) / 16) + 1;
int lineOffset;
int lineLength;
for (int i = 0; i < lines; i++) {
lineOffset = (i * 16) + offset;
lineLength = Math.min(16, (length - (i * 16)));
appendDumpLine(buf, i, bytes, lineOffset, lineLength);
buf.append(EOL);
}
return buf.toString();
}
private static void appendDumpLine(StringBuffer buf, int line, byte[] bytes, int lineOffset, int lineLength) {
buf.append(HEX_DIGITS[(line >> 28) & 0xf]);
buf.append(HEX_DIGITS[(line >> 24) & 0xf]);
buf.append(HEX_DIGITS[(line >> 20) & 0xf]);
buf.append(HEX_DIGITS[(line >> 16) & 0xf]);
buf.append(HEX_DIGITS[(line >> 12) & 0xf]);
buf.append(HEX_DIGITS[(line >> 8) & 0xf]);
buf.append(HEX_DIGITS[(line >> 4) & 0xf]);
buf.append(HEX_DIGITS[(line ) & 0xf]);
buf.append(": ");
for (int i = 0; i < 16; i++) {
int idx = i + lineOffset;
if (i < lineLength) {
int b = bytes[idx];
appendHexChar(buf, b);
} else {
buf.append(" ");
}
if ((i % 2) == 1) {
buf.append(' ');
}
}
for (int i = 0; i < 16 && i < lineLength; i++) {
int idx = i + lineOffset;
int b = bytes[idx];
if (b >= 0x20 && b <= 0x7e) {
buf.append((char)b);
} else {
buf.append('.');
}
}
}
private static void appendHexChar(StringBuffer buf, int b) {
buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
buf.append(HEX_DIGITS[b & 0xf]);
}
}

View File

@@ -1,23 +0,0 @@
package org.signal.core.util
import java.util.Optional
fun <E> Optional<E>.or(other: Optional<E>): Optional<E> {
return if (this.isPresent) {
this
} else {
other
}
}
fun <E> Optional<E>.isAbsent(): Boolean {
return !isPresent
}
fun <E : Any> E?.toOptional(): Optional<E> {
return Optional.ofNullable(this)
}
fun <E> Optional<E>.orNull(): E? {
return orElse(null)
}

View File

@@ -1,34 +0,0 @@
package org.signal.core.util;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
public final class SetUtil {
private SetUtil() {}
public static <E> Set<E> intersection(Collection<E> a, Collection<E> b) {
Set<E> intersection = new LinkedHashSet<>(a);
intersection.retainAll(b);
return intersection;
}
public static <E> Set<E> difference(Collection<E> a, Collection<E> b) {
Set<E> difference = new LinkedHashSet<>(a);
difference.removeAll(b);
return difference;
}
public static <E> Set<E> union(Set<E> a, Set<E> b) {
Set<E> result = new LinkedHashSet<>(a);
result.addAll(b);
return result;
}
@SafeVarargs
public static <E> HashSet<E> newHashSet(E... elements) {
return new HashSet<>(Arrays.asList(elements));
}
}

View File

@@ -1,74 +0,0 @@
package org.signal.core.util
import org.signal.core.util.logging.Log
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.DurationUnit
/**
* Simple utility to easily track the time a multi-step operation takes via splits.
*
* e.g.
*
* ```kotlin
* val stopwatch = Stopwatch("my-event")
* stopwatch.split("split-1")
* stopwatch.split("split-2")
* stopwatch.split("split-3")
* stopwatch.stop(TAG)
* ```
*/
class Stopwatch @JvmOverloads constructor(private val title: String, private val decimalPlaces: Int = 0) {
private val startTimeNanos: Long = System.nanoTime()
private val splits: MutableList<Split> = mutableListOf()
/**
* Create a new split between now and the last event.
*/
fun split(label: String) {
val now = System.nanoTime()
val previousTime = if (splits.isEmpty()) {
startTimeNanos
} else {
splits.last().nanoTime
}
splits += Split(
nanoTime = now,
durationNanos = now - previousTime,
label = label
)
}
/**
* Stops the stopwatch and logs the results with the provided tag.
*/
fun stop(tag: String) {
Log.d(tag, stopAndGetLogString())
}
/**
* Similar to [stop], but instead of logging directly, this will return the log string.
*/
fun stopAndGetLogString(): String {
val now = System.nanoTime()
splits += Split(
nanoTime = now,
durationNanos = now - startTimeNanos,
label = "total"
)
val splitString = splits
.joinToString(separator = ", ", transform = { it.displayString(decimalPlaces) })
return "[$title] $splitString"
}
private data class Split(val nanoTime: Long, val durationNanos: Long, val label: String) {
fun displayString(decimalPlaces: Int): String {
val timeMs: String = durationNanos.nanoseconds.toDouble(DurationUnit.MILLISECONDS).roundedString(decimalPlaces)
return "$label: $timeMs"
}
}
}

View File

@@ -2,6 +2,8 @@ package org.signal.core.util.logging;
import android.annotation.SuppressLint;
import org.signal.core.util.logging.Log;
@SuppressLint("LogNotSignal")
public final class AndroidLogger extends Log.Logger {

View File

@@ -1,128 +0,0 @@
package org.signal.core.util.logging;
import androidx.annotation.NonNull;
/**
* A way to treat N loggers as one. Wraps a bunch of other loggers and forwards the method calls to
* all of them.
*/
class CompoundLogger extends Log.Logger {
private final Log.Logger[] loggers;
CompoundLogger(@NonNull Log.Logger... loggers) {
this.loggers = loggers;
}
@Override
public void v(String tag, String message, Throwable t, boolean keepLonger) {
for (Log.Logger logger : loggers) {
logger.v(tag, message, t, keepLonger);
}
}
@Override
public void d(String tag, String message, Throwable t, boolean keepLonger) {
for (Log.Logger logger : loggers) {
logger.d(tag, message, t, keepLonger);
}
}
@Override
public void i(String tag, String message, Throwable t, boolean keepLonger) {
for (Log.Logger logger : loggers) {
logger.i(tag, message, t, keepLonger);
}
}
@Override
public void w(String tag, String message, Throwable t, boolean keepLonger) {
for (Log.Logger logger : loggers) {
logger.w(tag, message, t, keepLonger);
}
}
@Override
public void e(String tag, String message, Throwable t, boolean keepLonger) {
for (Log.Logger logger : loggers) {
logger.e(tag, message, t, keepLonger);
}
}
@Override
public void v(String tag, String message, Throwable t) {
for (Log.Logger logger :loggers) {
logger.v(tag, message, t);
}
}
@Override
public void d(String tag, String message, Throwable t) {
for (Log.Logger logger :loggers) {
logger.d(tag, message, t);
}
}
@Override
public void i(String tag, String message, Throwable t) {
for (Log.Logger logger :loggers) {
logger.i(tag, message, t);
}
}
@Override
public void w(String tag, String message, Throwable t) {
for (Log.Logger logger :loggers) {
logger.w(tag, message, t);
}
}
@Override
public void e(String tag, String message, Throwable t) {
for (Log.Logger logger :loggers) {
logger.e(tag, message, t);
}
}
@Override
public void v(String tag, String message) {
for (Log.Logger logger :loggers) {
logger.v(tag, message);
}
}
@Override
public void d(String tag, String message) {
for (Log.Logger logger :loggers) {
logger.d(tag, message);
}
}
@Override
public void i(String tag, String message) {
for (Log.Logger logger :loggers) {
logger.i(tag, message);
}
}
@Override
public void w(String tag, String message) {
for (Log.Logger logger :loggers) {
logger.w(tag, message);
}
}
@Override
public void e(String tag, String message) {
for (Log.Logger logger :loggers) {
logger.e(tag, message);
}
}
@Override
public void flush() {
for (Log.Logger logger : loggers) {
logger.flush();
}
}
}

View File

@@ -1,229 +0,0 @@
package org.signal.core.util.logging;
import android.annotation.SuppressLint;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
@SuppressLint("LogNotSignal")
public final class Log {
private static final Logger NOOP_LOGGER = new NoopLogger();
private static InternalCheck internalCheck;
private static Logger logger = new AndroidLogger();
/**
* @param internalCheck A checker that will indicate if this is an internal user
* @param loggers A list of loggers that will be given every log statement.
*/
@MainThread
public static void initialize(@NonNull InternalCheck internalCheck, Logger... loggers) {
Log.internalCheck = internalCheck;
Log.logger = new CompoundLogger(loggers);
}
public static void initialize(Logger... loggers) {
initialize(() -> false, loggers);
}
public static void v(String tag, String message) {
v(tag, message, null);
}
public static void d(String tag, String message) {
d(tag, message, null);
}
public static void i(String tag, String message) {
i(tag, message, null);
}
public static void w(String tag, String message) {
w(tag, message, null);
}
public static void e(String tag, String message) {
e(tag, message, null);
}
public static void v(String tag, Throwable t) {
v(tag, null, t);
}
public static void d(String tag, Throwable t) {
d(tag, null, t);
}
public static void i(String tag, Throwable t) {
i(tag, null, t);
}
public static void w(String tag, Throwable t) {
w(tag, null, t);
}
public static void e(String tag, Throwable t) {
e(tag, null, t);
}
public static void v(String tag, String message, Throwable t) {
logger.v(tag, message, t);
}
public static void d(String tag, String message, Throwable t) {
logger.d(tag, message, t);
}
public static void i(String tag, String message, Throwable t) {
logger.i(tag, message, t);
}
public static void w(String tag, String message, Throwable t) {
logger.w(tag, message, t);
}
public static void e(String tag, String message, Throwable t) {
logger.e(tag, message, t);
}
public static void v(String tag, String message, boolean keepLonger) {
logger.v(tag, message, keepLonger);
}
public static void d(String tag, String message, boolean keepLonger) {
logger.d(tag, message, keepLonger);
}
public static void i(String tag, String message, boolean keepLonger) {
logger.i(tag, message, keepLonger);
}
public static void w(String tag, String message, boolean keepLonger) {
logger.w(tag, message, keepLonger);
}
public static void e(String tag, String message, boolean keepLonger) {
logger.e(tag, message, keepLonger);
}
public static void v(String tag, String message, Throwable t, boolean keepLonger) {
logger.v(tag, message, t, keepLonger);
}
public static void d(String tag, String message, Throwable t, boolean keepLonger) {
logger.d(tag, message, t, keepLonger);
}
public static void i(String tag, String message, Throwable t, boolean keepLonger) {
logger.i(tag, message, t, keepLonger);
}
public static void w(String tag, String message, Throwable t, boolean keepLonger) {
logger.w(tag, message, t, keepLonger);
}
public static void e(String tag, String message, Throwable t, boolean keepLonger) {
logger.e(tag, message, t, keepLonger);
}
public static @NonNull String tag(Class<?> clazz) {
String simpleName = clazz.getSimpleName();
if (simpleName.length() > 23) {
return simpleName.substring(0, 23);
}
return simpleName;
}
/**
* Important: This is not something that can be used to log PII. Instead, it's intended use is for
* logs that might be too verbose or otherwise unnecessary for public users.
*
* @return The normal logger if this is an internal user, or a no-op logger if it isn't.
*/
public static Logger internal() {
if (internalCheck.isInternal()) {
return logger;
} else {
return NOOP_LOGGER;
}
}
public static void blockUntilAllWritesFinished() {
logger.flush();
}
public static abstract class Logger {
public abstract void v(String tag, String message, Throwable t, boolean keepLonger);
public abstract void d(String tag, String message, Throwable t, boolean keepLonger);
public abstract void i(String tag, String message, Throwable t, boolean keepLonger);
public abstract void w(String tag, String message, Throwable t, boolean keepLonger);
public abstract void e(String tag, String message, Throwable t, boolean keepLonger);
public abstract void flush();
public void v(String tag, String message, boolean keepLonger) {
v(tag, message, null, keepLonger);
}
public void d(String tag, String message, boolean keepLonger) {
d(tag, message, null, keepLonger);
}
public void i(String tag, String message, boolean keepLonger) {
i(tag, message, null, keepLonger);
}
public void w(String tag, String message, boolean keepLonger) {
w(tag, message, null, keepLonger);
}
public void e(String tag, String message, boolean keepLonger) {
e(tag, message, null, keepLonger);
}
public void v(String tag, String message, Throwable t) {
v(tag, message, t, false);
}
public void d(String tag, String message, Throwable t) {
d(tag, message, t, false);
}
public void i(String tag, String message, Throwable t) {
i(tag, message, t, false);
}
public void w(String tag, String message, Throwable t) {
w(tag, message, t, false);
}
public void e(String tag, String message, Throwable t) {
e(tag, message, t, false);
}
public void v(String tag, String message) {
v(tag, message, null);
}
public void d(String tag, String message) {
d(tag, message, null);
}
public void i(String tag, String message) {
i(tag, message, null);
}
public void w(String tag, String message) {
w(tag, message, null);
}
public void e(String tag, String message) {
e(tag, message, null);
}
}
public interface InternalCheck {
boolean isInternal();
}
}

View File

@@ -1,24 +0,0 @@
package org.signal.core.util.logging;
/**
* A logger that does nothing.
*/
class NoopLogger extends Log.Logger {
@Override
public void v(String tag, String message, Throwable t, boolean keepLonger) { }
@Override
public void d(String tag, String message, Throwable t, boolean keepLonger) { }
@Override
public void i(String tag, String message, Throwable t, boolean keepLonger) { }
@Override
public void w(String tag, String message, Throwable t, boolean keepLonger) { }
@Override
public void e(String tag, String message, Throwable t, boolean keepLonger) { }
@Override
public void flush() { }
}

View File

@@ -1,200 +0,0 @@
/*
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.signal.core.util.logging
import java.util.regex.Matcher
import java.util.regex.Pattern
/** Given a [Matcher], update the [StringBuilder] with the scrubbed output you want for a given match. */
private typealias MatchProcessor = (Matcher, StringBuilder) -> Unit
/**
* Scrub data for possibly sensitive information.
*/
object Scrubber {
/**
* The middle group will be censored.
* Supposedly, the shortest international phone numbers in use contain seven digits.
* Handles URL encoded +, %2B
*/
private val E164_PATTERN = Pattern.compile("(\\+|%2B)(\\d{5,13})(\\d{2})")
private const val E164_CENSOR = "*************"
/** The second group will be censored.*/
private val CRUDE_EMAIL_PATTERN = Pattern.compile("\\b([^\\s/])([^\\s/]*@[^\\s]+)")
private const val EMAIL_CENSOR = "...@..."
/** The middle group will be censored. */
private val GROUP_ID_V1_PATTERN = Pattern.compile("(__)(textsecure_group__![^\\s]+)([^\\s]{2})")
private const val GROUP_ID_V1_CENSOR = "...group..."
/** The middle group will be censored. */
private val GROUP_ID_V2_PATTERN = Pattern.compile("(__)(signal_group__v2__![^\\s]+)([^\\s]{2})")
private const val GROUP_ID_V2_CENSOR = "...group_v2..."
/** The middle group will be censored. */
private val UUID_PATTERN = Pattern.compile("(JOB::)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{9})([0-9a-f]{3})", Pattern.CASE_INSENSITIVE)
private const val UUID_CENSOR = "********-****-****-****-*********"
/**
* The entire string is censored. Note: left as concatenated strings because kotlin string literals leave trailing newlines, and removing them breaks
* syntax highlighting.
*/
private val IPV4_PATTERN = Pattern.compile(
"\\b" +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" +
"\\b"
)
private const val IPV4_CENSOR = "...ipv4..."
/** The entire string is censored. */
private val IPV6_PATTERN = Pattern.compile("([0-9a-fA-F]{0,4}:){3,7}([0-9a-fA-F]){0,4}")
private const val IPV6_CENSOR = "...ipv6..."
/** The domain name except for TLD will be censored. */
private val DOMAIN_PATTERN = Pattern.compile("([a-z0-9]+\\.)+([a-z0-9\\-]*[a-z\\-][a-z0-9\\-]*)", Pattern.CASE_INSENSITIVE)
private const val DOMAIN_CENSOR = "***."
private val TOP_100_TLDS: Set<String> = setOf(
"com", "net", "org", "jp", "de", "uk", "fr", "br", "it", "ru", "es", "me", "gov", "pl", "ca", "au", "cn", "co", "in",
"nl", "edu", "info", "eu", "ch", "id", "at", "kr", "cz", "mx", "be", "tv", "se", "tr", "tw", "al", "ua", "ir", "vn",
"cl", "sk", "ly", "cc", "to", "no", "fi", "us", "pt", "dk", "ar", "hu", "tk", "gr", "il", "news", "ro", "my", "biz",
"ie", "za", "nz", "sg", "ee", "th", "io", "xyz", "pe", "bg", "hk", "lt", "link", "ph", "club", "si", "site",
"mobi", "by", "cat", "wiki", "la", "ga", "xxx", "cf", "hr", "ng", "jobs", "online", "kz", "ug", "gq", "ae", "is",
"lv", "pro", "fm", "tips", "ms", "sa", "app"
)
/** Base16 Call Link Key Pattern */
private val CALL_LINK_PATTERN = Pattern.compile("([bBcCdDfFgGhHkKmMnNpPqQrRsStTxXzZ]{4})(-[bBcCdDfFgGhHkKmMnNpPqQrRsStTxXzZ]{4}){7}")
private const val CALL_LINK_CENSOR_SUFFIX = "-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
@JvmStatic
fun scrub(input: CharSequence): CharSequence {
return input
.scrubE164()
.scrubEmail()
.scrubGroupsV1()
.scrubGroupsV2()
.scrubUuids()
.scrubDomains()
.scrubIpv4()
.scrubIpv6()
.scrubCallLinkKeys()
}
private fun CharSequence.scrubE164(): CharSequence {
return scrub(this, E164_PATTERN) { matcher, output ->
output
.append(matcher.group(1))
.append(E164_CENSOR, 0, matcher.group(2)!!.length)
.append(matcher.group(3))
}
}
private fun CharSequence.scrubEmail(): CharSequence {
return scrub(this, CRUDE_EMAIL_PATTERN) { matcher, output ->
output
.append(matcher.group(1))
.append(EMAIL_CENSOR)
}
}
private fun CharSequence.scrubGroupsV1(): CharSequence {
return scrub(this, GROUP_ID_V1_PATTERN) { matcher, output ->
output
.append(matcher.group(1))
.append(GROUP_ID_V1_CENSOR)
.append(matcher.group(3))
}
}
private fun CharSequence.scrubGroupsV2(): CharSequence {
return scrub(this, GROUP_ID_V2_PATTERN) { matcher, output ->
output
.append(matcher.group(1))
.append(GROUP_ID_V2_CENSOR)
.append(matcher.group(3))
}
}
private fun CharSequence.scrubUuids(): CharSequence {
return scrub(this, UUID_PATTERN) { matcher, output ->
if (matcher.group(1) != null && matcher.group(1)!!.isNotEmpty()) {
output
.append(matcher.group(1))
.append(matcher.group(2))
.append(matcher.group(3))
} else {
output
.append(UUID_CENSOR)
.append(matcher.group(3))
}
}
}
private fun CharSequence.scrubDomains(): CharSequence {
return scrub(this, DOMAIN_PATTERN) { matcher, output ->
val match: String = matcher.group(0)!!
if (matcher.groupCount() == 2 && TOP_100_TLDS.contains(matcher.group(2)!!.lowercase()) && !match.endsWith("signal.org")) {
output
.append(DOMAIN_CENSOR)
.append(matcher.group(2))
} else {
output.append(match)
}
}
}
private fun CharSequence.scrubIpv4(): CharSequence {
return scrub(this, IPV4_PATTERN) { _, output -> output.append(IPV4_CENSOR) }
}
private fun CharSequence.scrubIpv6(): CharSequence {
return scrub(this, IPV6_PATTERN) { _, output -> output.append(IPV6_CENSOR) }
}
private fun CharSequence.scrubCallLinkKeys(): CharSequence {
return scrub(this, CALL_LINK_PATTERN) { matcher, output ->
val match = matcher.group(1)
output
.append(match)
.append(CALL_LINK_CENSOR_SUFFIX)
}
}
private fun scrub(input: CharSequence, pattern: Pattern, processMatch: MatchProcessor): CharSequence {
val output = StringBuilder(input.length)
val matcher: Matcher = pattern.matcher(input)
var lastEndingPos = 0
while (matcher.find()) {
output.append(input, lastEndingPos, matcher.start())
processMatch(matcher, output)
lastEndingPos = matcher.end()
}
return if (lastEndingPos == 0) {
// there were no matches, save copying all the data
input
} else {
output.append(input, lastEndingPos, input.length)
output
}
}
}