1/*
2 * Copyright (c) 2018, Daniel Gultsch All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without modification,
5 * are permitted provided that the following conditions are met:
6 *
7 * 1. Redistributions of source code must retain the above copyright notice, this
8 * list of conditions and the following disclaimer.
9 *
10 * 2. Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation and/or
12 * other materials provided with the distribution.
13 *
14 * 3. Neither the name of the copyright holder nor the names of its contributors
15 * may be used to endorse or promote products derived from this software without
16 * specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30package eu.siacs.conversations.utils;
31
32import android.annotation.TargetApi;
33import android.content.Context;
34import android.os.Build;
35import android.support.annotation.ColorInt;
36import android.text.Spannable;
37import android.text.SpannableString;
38import android.text.SpannableStringBuilder;
39import android.text.style.ForegroundColorSpan;
40import android.util.LruCache;
41
42import java.util.ArrayList;
43import java.util.Collections;
44import java.util.HashMap;
45import java.util.HashSet;
46import java.util.List;
47import java.util.Map;
48import java.util.Set;
49import java.util.regex.Matcher;
50import java.util.regex.Pattern;
51
52import eu.siacs.conversations.R;
53import eu.siacs.conversations.ui.util.Color;
54import rocks.xmpp.addr.Jid;
55
56public class IrregularUnicodeDetector {
57
58 private static final Map<Character.UnicodeBlock, Character.UnicodeBlock> NORMALIZATION_MAP;
59 private static final LruCache<Jid, PatternTuple> CACHE = new LruCache<>(100);
60
61 static {
62 Map<Character.UnicodeBlock, Character.UnicodeBlock> temp = new HashMap<>();
63 temp.put(Character.UnicodeBlock.LATIN_1_SUPPLEMENT, Character.UnicodeBlock.BASIC_LATIN);
64 NORMALIZATION_MAP = Collections.unmodifiableMap(temp);
65 }
66
67 private static Character.UnicodeBlock normalize(Character.UnicodeBlock in) {
68 if (NORMALIZATION_MAP.containsKey(in)) {
69 return NORMALIZATION_MAP.get(in);
70 } else {
71 return in;
72 }
73 }
74
75 public static Spannable style(Context context, Jid jid) {
76 return style(jid, Color.get(context, R.attr.color_warning));
77 }
78
79 private static Spannable style(Jid jid, @ColorInt int color) {
80 PatternTuple patternTuple = find(jid);
81 SpannableStringBuilder builder = new SpannableStringBuilder();
82 if (jid.getLocal() != null && patternTuple.local != null) {
83 SpannableString local = new SpannableString(jid.getLocal());
84 colorize(local, patternTuple.local, color);
85 builder.append(local);
86 builder.append('@');
87 }
88 if (jid.getDomain() != null) {
89 int i = jid.getDomain().lastIndexOf('.');
90 if (i != -1) {
91 String second = jid.getDomain().substring(0, i);
92 String top = jid.getDomain().substring(i, jid.getDomain().length());
93 SpannableString secondSpannableString = new SpannableString(second);
94 colorize(secondSpannableString, patternTuple.domain, color);
95 builder.append(secondSpannableString);
96 builder.append(top);
97 } else {
98 builder.append(jid.getDomain());
99 }
100 }
101 if (builder.length() != 0 && jid.getResource() != null) {
102 builder.append('/');
103 builder.append(jid.getResource());
104 }
105 return builder;
106 }
107
108 private static void colorize(SpannableString spannableString, Pattern pattern, @ColorInt int color) {
109 Matcher matcher = pattern.matcher(spannableString);
110 while (matcher.find()) {
111 if (matcher.start() < matcher.end()) {
112 spannableString.setSpan(new ForegroundColorSpan(color), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
113 }
114 }
115 }
116
117 private static Map<Character.UnicodeBlock, List<String>> mapCompat(String word) {
118 Map<Character.UnicodeBlock, List<String>> map = new HashMap<>();
119 final int length = word.length();
120 for (int offset = 0; offset < length; ) {
121 final int codePoint = word.codePointAt(offset);
122 Character.UnicodeBlock block = normalize(Character.UnicodeBlock.of(codePoint));
123 List<String> codePoints;
124 if (map.containsKey(block)) {
125 codePoints = map.get(block);
126 } else {
127 codePoints = new ArrayList<>();
128 map.put(block, codePoints);
129 }
130 codePoints.add(String.copyValueOf(Character.toChars(codePoint)));
131 offset += Character.charCount(codePoint);
132 }
133 return map;
134 }
135
136 @TargetApi(Build.VERSION_CODES.N)
137 private static Map<Character.UnicodeScript, List<String>> map(String word) {
138 Map<Character.UnicodeScript, List<String>> map = new HashMap<>();
139 final int length = word.length();
140 for (int offset = 0; offset < length; ) {
141 final int codePoint = word.codePointAt(offset);
142 Character.UnicodeScript script = Character.UnicodeScript.of(codePoint);
143 if (script != Character.UnicodeScript.COMMON) {
144 List<String> codePoints;
145 if (map.containsKey(script)) {
146 codePoints = map.get(script);
147 } else {
148 codePoints = new ArrayList<>();
149 map.put(script, codePoints);
150 }
151 codePoints.add(String.copyValueOf(Character.toChars(codePoint)));
152 }
153 offset += Character.charCount(codePoint);
154 }
155 return map;
156 }
157
158 private static Set<String> eliminateFirstAndGetCodePointsCompat(Map<Character.UnicodeBlock, List<String>> map) {
159 return eliminateFirstAndGetCodePoints(map, Character.UnicodeBlock.BASIC_LATIN);
160 }
161
162 @TargetApi(Build.VERSION_CODES.N)
163 private static Set<String> eliminateFirstAndGetCodePoints(Map<Character.UnicodeScript, List<String>> map) {
164 return eliminateFirstAndGetCodePoints(map, Character.UnicodeScript.COMMON);
165 }
166
167 private static <T> Set<String> eliminateFirstAndGetCodePoints(Map<T, List<String>> map, T defaultPick) {
168 T pick = defaultPick;
169 int size = 0;
170 for (Map.Entry<T, List<String>> entry : map.entrySet()) {
171 if (entry.getValue().size() > size) {
172 size = entry.getValue().size();
173 pick = entry.getKey();
174 }
175 }
176 map.remove(pick);
177 Set<String> all = new HashSet<>();
178 for (List<String> codePoints : map.values()) {
179 all.addAll(codePoints);
180 }
181 return all;
182 }
183
184 private static Set<String> findIrregularCodePoints(String word) {
185 Set<String> codePoints;
186 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
187 codePoints = eliminateFirstAndGetCodePointsCompat(mapCompat(word));
188 } else {
189 codePoints = eliminateFirstAndGetCodePoints(map(word));
190 }
191 return codePoints;
192 }
193
194 private static PatternTuple find(Jid jid) {
195 synchronized (CACHE) {
196 PatternTuple pattern = CACHE.get(jid);
197 if (pattern != null) {
198 return pattern;
199 }
200 ;
201 pattern = PatternTuple.of(jid);
202 CACHE.put(jid, pattern);
203 return pattern;
204 }
205 }
206
207 private static Pattern create(Set<String> codePoints) {
208 final StringBuilder pattern = new StringBuilder();
209 for (String codePoint : codePoints) {
210 if (pattern.length() != 0) {
211 pattern.append('|');
212 }
213 pattern.append(Pattern.quote(codePoint));
214 }
215 return Pattern.compile(pattern.toString());
216 }
217
218 private static class PatternTuple {
219 private final Pattern local;
220 private final Pattern domain;
221
222 private PatternTuple(Pattern local, Pattern domain) {
223 this.local = local;
224 this.domain = domain;
225 }
226
227 private static PatternTuple of(Jid jid) {
228 final Pattern localPattern;
229 if (jid.getLocal() != null) {
230 localPattern = create(findIrregularCodePoints(jid.getLocal()));
231 } else {
232 localPattern = null;
233 }
234 String domain = jid.getDomain();
235 final Pattern domainPattern;
236 if (domain != null) {
237 int i = domain.lastIndexOf('.');
238 if (i != -1) {
239 String secondLevel = domain.substring(0, i);
240 domainPattern = create(findIrregularCodePoints(secondLevel));
241 } else {
242 domainPattern = null;
243 }
244 } else {
245 domainPattern = null;
246 }
247 return new PatternTuple(localPattern, domainPattern);
248 }
249 }
250}