2018-03-08 13:02:48 +00:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2018, Daniel Gultsch All rights reserved.
|
|
|
|
*
|
|
|
|
* Redistribution and use in source and binary forms, with or without modification,
|
|
|
|
* are permitted provided that the following conditions are met:
|
|
|
|
*
|
|
|
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
|
|
* list of conditions and the following disclaimer.
|
|
|
|
*
|
|
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
|
|
* this list of conditions and the following disclaimer in the documentation and/or
|
|
|
|
* other materials provided with the distribution.
|
|
|
|
*
|
|
|
|
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
|
|
* may be used to endorse or promote products derived from this software without
|
|
|
|
* specific prior written permission.
|
|
|
|
*
|
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
|
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
|
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package eu.siacs.conversations.utils;
|
|
|
|
|
2018-03-08 19:45:30 +00:00
|
|
|
import android.annotation.TargetApi;
|
2018-03-08 13:02:48 +00:00
|
|
|
import android.content.Context;
|
2018-03-08 19:45:30 +00:00
|
|
|
import android.os.Build;
|
2018-03-08 13:02:48 +00:00
|
|
|
import android.support.annotation.ColorInt;
|
|
|
|
import android.text.Spannable;
|
|
|
|
import android.text.SpannableString;
|
|
|
|
import android.text.SpannableStringBuilder;
|
|
|
|
import android.text.style.ForegroundColorSpan;
|
|
|
|
import android.util.LruCache;
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.HashSet;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.Map;
|
|
|
|
import java.util.Set;
|
|
|
|
import java.util.regex.Matcher;
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
import eu.siacs.conversations.R;
|
|
|
|
import eu.siacs.conversations.ui.util.Color;
|
|
|
|
import rocks.xmpp.addr.Jid;
|
|
|
|
|
2018-03-08 21:02:19 +00:00
|
|
|
public class IrregularUnicodeDetector {
|
2018-03-08 13:02:48 +00:00
|
|
|
|
2018-03-08 19:45:30 +00:00
|
|
|
private static final Map<Character.UnicodeBlock, Character.UnicodeBlock> NORMALIZATION_MAP;
|
2018-03-19 12:32:32 +00:00
|
|
|
private static final LruCache<Jid, PatternTuple> CACHE = new LruCache<>(4096);
|
2018-03-08 13:02:48 +00:00
|
|
|
|
|
|
|
static {
|
2018-03-08 19:45:30 +00:00
|
|
|
Map<Character.UnicodeBlock, Character.UnicodeBlock> temp = new HashMap<>();
|
2018-03-08 13:02:48 +00:00
|
|
|
temp.put(Character.UnicodeBlock.LATIN_1_SUPPLEMENT, Character.UnicodeBlock.BASIC_LATIN);
|
|
|
|
NORMALIZATION_MAP = Collections.unmodifiableMap(temp);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Character.UnicodeBlock normalize(Character.UnicodeBlock in) {
|
|
|
|
if (NORMALIZATION_MAP.containsKey(in)) {
|
|
|
|
return NORMALIZATION_MAP.get(in);
|
|
|
|
} else {
|
|
|
|
return in;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static Spannable style(Context context, Jid jid) {
|
|
|
|
return style(jid, Color.get(context, R.attr.color_warning));
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Spannable style(Jid jid, @ColorInt int color) {
|
2018-03-08 21:02:19 +00:00
|
|
|
PatternTuple patternTuple = find(jid);
|
2018-03-08 13:02:48 +00:00
|
|
|
SpannableStringBuilder builder = new SpannableStringBuilder();
|
2018-03-08 21:02:19 +00:00
|
|
|
if (jid.getLocal() != null && patternTuple.local != null) {
|
2018-03-08 13:02:48 +00:00
|
|
|
SpannableString local = new SpannableString(jid.getLocal());
|
2018-03-08 21:02:19 +00:00
|
|
|
colorize(local, patternTuple.local, color);
|
2018-03-08 13:02:48 +00:00
|
|
|
builder.append(local);
|
|
|
|
builder.append('@');
|
|
|
|
}
|
|
|
|
if (jid.getDomain() != null) {
|
2018-03-09 20:39:10 +00:00
|
|
|
String[] labels = jid.getDomain().split("\\.");
|
|
|
|
for (int i = 0; i < labels.length; ++i) {
|
|
|
|
SpannableString spannableString = new SpannableString(labels[i]);
|
|
|
|
colorize(spannableString, patternTuple.domain.get(i), color);
|
|
|
|
if (i != 0) {
|
|
|
|
builder.append('.');
|
|
|
|
}
|
|
|
|
builder.append(spannableString);
|
2018-03-08 21:02:19 +00:00
|
|
|
}
|
2018-03-08 13:02:48 +00:00
|
|
|
}
|
|
|
|
if (builder.length() != 0 && jid.getResource() != null) {
|
|
|
|
builder.append('/');
|
|
|
|
builder.append(jid.getResource());
|
|
|
|
}
|
|
|
|
return builder;
|
|
|
|
}
|
|
|
|
|
2018-03-08 21:02:19 +00:00
|
|
|
private static void colorize(SpannableString spannableString, Pattern pattern, @ColorInt int color) {
|
|
|
|
Matcher matcher = pattern.matcher(spannableString);
|
|
|
|
while (matcher.find()) {
|
|
|
|
if (matcher.start() < matcher.end()) {
|
|
|
|
spannableString.setSpan(new ForegroundColorSpan(color), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Map<Character.UnicodeBlock, List<String>> mapCompat(String word) {
|
2018-03-08 13:02:48 +00:00
|
|
|
Map<Character.UnicodeBlock, List<String>> map = new HashMap<>();
|
2018-03-08 21:02:19 +00:00
|
|
|
final int length = word.length();
|
2018-03-08 13:02:48 +00:00
|
|
|
for (int offset = 0; offset < length; ) {
|
2018-03-08 21:02:19 +00:00
|
|
|
final int codePoint = word.codePointAt(offset);
|
2018-04-11 07:26:56 +00:00
|
|
|
offset += Character.charCount(codePoint);
|
|
|
|
if (!Character.isLetter(codePoint)) {
|
|
|
|
continue;
|
|
|
|
}
|
2018-03-08 13:02:48 +00:00
|
|
|
Character.UnicodeBlock block = normalize(Character.UnicodeBlock.of(codePoint));
|
|
|
|
List<String> codePoints;
|
|
|
|
if (map.containsKey(block)) {
|
|
|
|
codePoints = map.get(block);
|
|
|
|
} else {
|
|
|
|
codePoints = new ArrayList<>();
|
|
|
|
map.put(block, codePoints);
|
|
|
|
}
|
|
|
|
codePoints.add(String.copyValueOf(Character.toChars(codePoint)));
|
|
|
|
}
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
|
2018-03-08 19:45:30 +00:00
|
|
|
@TargetApi(Build.VERSION_CODES.N)
|
2018-03-08 21:02:19 +00:00
|
|
|
private static Map<Character.UnicodeScript, List<String>> map(String word) {
|
2018-03-08 19:45:30 +00:00
|
|
|
Map<Character.UnicodeScript, List<String>> map = new HashMap<>();
|
2018-03-08 21:02:19 +00:00
|
|
|
final int length = word.length();
|
2018-03-08 19:45:30 +00:00
|
|
|
for (int offset = 0; offset < length; ) {
|
2018-03-08 21:02:19 +00:00
|
|
|
final int codePoint = word.codePointAt(offset);
|
2018-03-08 19:45:30 +00:00
|
|
|
Character.UnicodeScript script = Character.UnicodeScript.of(codePoint);
|
|
|
|
if (script != Character.UnicodeScript.COMMON) {
|
|
|
|
List<String> codePoints;
|
|
|
|
if (map.containsKey(script)) {
|
|
|
|
codePoints = map.get(script);
|
|
|
|
} else {
|
|
|
|
codePoints = new ArrayList<>();
|
|
|
|
map.put(script, codePoints);
|
|
|
|
}
|
|
|
|
codePoints.add(String.copyValueOf(Character.toChars(codePoint)));
|
|
|
|
}
|
|
|
|
offset += Character.charCount(codePoint);
|
|
|
|
}
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Set<String> eliminateFirstAndGetCodePointsCompat(Map<Character.UnicodeBlock, List<String>> map) {
|
|
|
|
return eliminateFirstAndGetCodePoints(map, Character.UnicodeBlock.BASIC_LATIN);
|
|
|
|
}
|
|
|
|
|
|
|
|
@TargetApi(Build.VERSION_CODES.N)
|
|
|
|
private static Set<String> eliminateFirstAndGetCodePoints(Map<Character.UnicodeScript, List<String>> map) {
|
|
|
|
return eliminateFirstAndGetCodePoints(map, Character.UnicodeScript.COMMON);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static <T> Set<String> eliminateFirstAndGetCodePoints(Map<T, List<String>> map, T defaultPick) {
|
|
|
|
T pick = defaultPick;
|
2018-03-08 13:02:48 +00:00
|
|
|
int size = 0;
|
2018-03-08 19:45:30 +00:00
|
|
|
for (Map.Entry<T, List<String>> entry : map.entrySet()) {
|
2018-03-08 13:02:48 +00:00
|
|
|
if (entry.getValue().size() > size) {
|
|
|
|
size = entry.getValue().size();
|
2018-03-08 19:45:30 +00:00
|
|
|
pick = entry.getKey();
|
2018-03-08 13:02:48 +00:00
|
|
|
}
|
|
|
|
}
|
2018-03-08 19:45:30 +00:00
|
|
|
map.remove(pick);
|
2018-03-08 13:02:48 +00:00
|
|
|
Set<String> all = new HashSet<>();
|
|
|
|
for (List<String> codePoints : map.values()) {
|
|
|
|
all.addAll(codePoints);
|
|
|
|
}
|
|
|
|
return all;
|
|
|
|
}
|
|
|
|
|
2018-03-08 21:02:19 +00:00
|
|
|
private static Set<String> findIrregularCodePoints(String word) {
|
|
|
|
Set<String> codePoints;
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
|
|
codePoints = eliminateFirstAndGetCodePointsCompat(mapCompat(word));
|
|
|
|
} else {
|
|
|
|
codePoints = eliminateFirstAndGetCodePoints(map(word));
|
|
|
|
}
|
|
|
|
return codePoints;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static PatternTuple find(Jid jid) {
|
2018-03-08 13:02:48 +00:00
|
|
|
synchronized (CACHE) {
|
2018-03-08 21:02:19 +00:00
|
|
|
PatternTuple pattern = CACHE.get(jid);
|
2018-03-08 13:02:48 +00:00
|
|
|
if (pattern != null) {
|
|
|
|
return pattern;
|
|
|
|
}
|
2018-03-08 21:02:19 +00:00
|
|
|
;
|
|
|
|
pattern = PatternTuple.of(jid);
|
2018-03-08 13:02:48 +00:00
|
|
|
CACHE.put(jid, pattern);
|
|
|
|
return pattern;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Pattern create(Set<String> codePoints) {
|
|
|
|
final StringBuilder pattern = new StringBuilder();
|
|
|
|
for (String codePoint : codePoints) {
|
|
|
|
if (pattern.length() != 0) {
|
|
|
|
pattern.append('|');
|
|
|
|
}
|
|
|
|
pattern.append(Pattern.quote(codePoint));
|
|
|
|
}
|
|
|
|
return Pattern.compile(pattern.toString());
|
|
|
|
}
|
2018-03-08 21:02:19 +00:00
|
|
|
|
|
|
|
private static class PatternTuple {
|
|
|
|
private final Pattern local;
|
2018-03-09 20:39:10 +00:00
|
|
|
private final List<Pattern> domain;
|
2018-03-08 21:02:19 +00:00
|
|
|
|
2018-03-09 20:39:10 +00:00
|
|
|
private PatternTuple(Pattern local, List<Pattern> domain) {
|
2018-03-08 21:02:19 +00:00
|
|
|
this.local = local;
|
|
|
|
this.domain = domain;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static PatternTuple of(Jid jid) {
|
|
|
|
final Pattern localPattern;
|
|
|
|
if (jid.getLocal() != null) {
|
|
|
|
localPattern = create(findIrregularCodePoints(jid.getLocal()));
|
|
|
|
} else {
|
|
|
|
localPattern = null;
|
|
|
|
}
|
|
|
|
String domain = jid.getDomain();
|
2018-03-09 20:39:10 +00:00
|
|
|
final List<Pattern> domainPatterns = new ArrayList<>();
|
2018-03-08 21:02:19 +00:00
|
|
|
if (domain != null) {
|
2018-03-09 20:39:10 +00:00
|
|
|
for (String label : domain.split("\\.")) {
|
|
|
|
domainPatterns.add(create(findIrregularCodePoints(label)));
|
2018-03-08 21:02:19 +00:00
|
|
|
}
|
|
|
|
}
|
2018-03-09 20:39:10 +00:00
|
|
|
return new PatternTuple(localPattern, domainPatterns);
|
2018-03-08 21:02:19 +00:00
|
|
|
}
|
|
|
|
}
|
2018-03-08 13:02:48 +00:00
|
|
|
}
|