001/**
002 *
003 * Copyright 2003-2007 Jive Software, 2016-2024 Florian Schmaus.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smack.util;
019
020import java.io.IOException;
021import java.nio.CharBuffer;
022import java.nio.charset.StandardCharsets;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Random;
029import java.util.regex.Pattern;
030
031/**
032 * A collection of utility methods for String objects.
033 */
034public class StringUtils {
035
036    public static final String MD5 = "MD5";
037    public static final String SHA1 = "SHA-1";
038
039    public static final String QUOTE_ENCODE = """;
040    public static final String APOS_ENCODE = "'";
041    public static final String AMP_ENCODE = "&";
042    public static final String LT_ENCODE = "<";
043    public static final String GT_ENCODE = ">";
044
045    public static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
046
047    /**
048     * Escape <code>input</code> for XML.
049     *
050     * @param input the input to escape.
051     * @return the XML escaped variant of <code>input</code>.
052     */
053    public static CharSequence escapeForXml(CharSequence input) {
054        return escapeForXml(input, XmlEscapeMode.safe);
055    }
056
057    /**
058     * Escape <code>input</code> for XML.
059     *
060     * @param input the input to escape.
061     * @return the XML escaped variant of <code>input</code>.
062     * @since 4.2
063     */
064    public static CharSequence escapeForXmlAttribute(CharSequence input) {
065        return escapeForXml(input, XmlEscapeMode.forAttribute);
066    }
067
068    /**
069     * Escape <code>input</code> for XML.
070     * <p>
071     * This is an optimized variant of {@link #escapeForXmlAttribute(CharSequence)} for XML where the
072     * XML attribute is quoted using ''' (Apos).
073     * </p>
074     *
075     * @param input the input to escape.
076     * @return the XML escaped variant of <code>input</code>.
077     * @since 4.2
078     */
079    public static CharSequence escapeForXmlAttributeApos(CharSequence input) {
080        return escapeForXml(input, XmlEscapeMode.forAttributeApos);
081    }
082
083    /**
084     * Escape <code>input</code> for XML.
085     *
086     * @param input the input to escape.
087     * @return the XML escaped variant of <code>input</code>.
088     * @since 4.2
089     */
090    public static CharSequence escapeForXmlText(CharSequence input) {
091        return escapeForXml(input, XmlEscapeMode.forText);
092    }
093
094    private enum XmlEscapeMode {
095        safe,
096        forAttribute,
097        forAttributeApos,
098        forText,
099    }
100
101    /**
102     * Escapes all necessary characters in the CharSequence so that it can be used
103     * in an XML doc.
104     *
105     * @param input the CharSequence to escape.
106     * @return the string with appropriate characters escaped.
107     */
108    private static CharSequence escapeForXml(final CharSequence input, final XmlEscapeMode xmlEscapeMode) {
109        if (input == null) {
110            return null;
111        }
112        final int len = input.length();
113        final StringBuilder out = new StringBuilder((int) (len * 1.3));
114        CharSequence toAppend;
115        char ch;
116        int last = 0;
117        int i = 0;
118        while (i < len) {
119            toAppend = null;
120            ch = input.charAt(i);
121            switch (xmlEscapeMode) {
122            case safe:
123                switch (ch) {
124                case '<':
125                    toAppend = LT_ENCODE;
126                    break;
127                case '>':
128                    toAppend = GT_ENCODE;
129                    break;
130                case '&':
131                    toAppend = AMP_ENCODE;
132                    break;
133                case '"':
134                    toAppend = QUOTE_ENCODE;
135                    break;
136                case '\'':
137                    toAppend = APOS_ENCODE;
138                    break;
139                default:
140                    break;
141                }
142                break;
143            case forAttribute:
144                // No need to escape '>' for attributes.
145                switch (ch) {
146                case '<':
147                    toAppend = LT_ENCODE;
148                    break;
149                case '&':
150                    toAppend = AMP_ENCODE;
151                    break;
152                case '"':
153                    toAppend = QUOTE_ENCODE;
154                    break;
155                case '\'':
156                    toAppend = APOS_ENCODE;
157                    break;
158                default:
159                    break;
160                }
161                break;
162            case forAttributeApos:
163                // No need to escape '>' and '"' for attributes using '\'' as quote.
164                switch (ch) {
165                case '<':
166                    toAppend = LT_ENCODE;
167                    break;
168                case '&':
169                    toAppend = AMP_ENCODE;
170                    break;
171                case '\'':
172                    toAppend = APOS_ENCODE;
173                    break;
174                default:
175                    break;
176                }
177                break;
178            case forText:
179                // No need to escape '"', '\'', and '>' for text.
180                switch (ch) {
181                case '<':
182                    toAppend = LT_ENCODE;
183                    break;
184                case '&':
185                    toAppend = AMP_ENCODE;
186                    break;
187                default:
188                    break;
189                }
190                break;
191            }
192            if (toAppend != null) {
193                if (i > last) {
194                    out.append(input, last, i);
195                }
196                out.append(toAppend);
197                last = ++i;
198            } else {
199                i++;
200            }
201        }
202        if (last == 0) {
203            return input;
204        }
205        if (i > last) {
206            out.append(input, last, i);
207        }
208        return out;
209    }
210
211    /**
212     * Hashes a String using the SHA-1 algorithm and returns the result as a
213     * String of hexadecimal numbers. This method is synchronized to avoid
214     * excessive MessageDigest object creation. If calling this method becomes
215     * a bottleneck in your code, you may wish to maintain a pool of
216     * MessageDigest objects instead of using this method.
217     * <p>
218     * A hash is a one-way function -- that is, given an
219     * input, an output is easily computed. However, given the output, the
220     * input is almost impossible to compute. This is useful for passwords
221     * since we can store the hash and a hacker will then have a very hard time
222     * determining the original password.
223     *
224     * @param data the String to compute the hash of.
225     * @return a hashed version of the passed-in String
226     * @deprecated use {@link org.jivesoftware.smack.util.SHA1#hex(String)} instead.
227     */
228    @Deprecated
229    public static synchronized String hash(String data) {
230        return org.jivesoftware.smack.util.SHA1.hex(data);
231    }
232
233    /**
234     * Encodes an array of bytes as String representation of hexadecimal.
235     *
236     * @param bytes an array of bytes to convert to a hex string.
237     * @return generated hex string.
238     */
239    public static String encodeHex(byte[] bytes) {
240        char[] hexChars = new char[bytes.length * 2];
241        for (int j = 0; j < bytes.length; j++) {
242            int v = bytes[j] & 0xFF;
243            hexChars[j * 2] = HEX_CHARS[v >>> 4];
244            hexChars[j * 2 + 1] = HEX_CHARS[v & 0x0F];
245        }
246        return new String(hexChars);
247    }
248
249    public static byte[] toUtf8Bytes(String string) {
250        return string.getBytes(StandardCharsets.UTF_8);
251    }
252
253    /**
254     * 24 upper case characters from the latin alphabet and numbers without '0' and 'O'.
255     */
256    public static final String UNAMBIGUOUS_NUMBERS_AND_LETTERS_STRING = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
257
258    /**
259     * 24 upper case characters from the latin alphabet and numbers without '0' and 'O'.
260     */
261    private static final char[] UNAMBIGUOUS_NUMBERS_AND_LETTERS = UNAMBIGUOUS_NUMBERS_AND_LETTERS_STRING.toCharArray();
262
263    /**
264     * Returns a random String of numbers and letters (lower and upper case)
265     * of the specified length. The method uses the Random class that is
266     * built-in to Java which is suitable for low to medium grade security uses.
267     * This means that the output is only pseudo random, i.e., each number is
268     * mathematically generated so is not truly random.<p>
269     *
270     * The specified length must be at least one. If not, the method will return
271     * null.
272     *
273     * @param length the desired length of the random String to return.
274     * @return a random String of numbers and letters of the specified length.
275     */
276    public static String insecureRandomString(int length) {
277        return randomString(length, RandomUtil.RANDOM.get());
278    }
279
280    public static String secureOnlineAttackSafeRandomString() {
281        // 34^10 = 2.06e15 possible combinations. Which is enough to protect against online brute force attacks.
282        // See also https://www.grc.com/haystack.htm
283        final int REQUIRED_LENGTH = 10;
284
285        return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTERS, REQUIRED_LENGTH);
286    }
287
288    public static String secureUniqueRandomString() {
289        // 34^13 = 8.11e19 possible combinations, which is > 2^64.
290        final int REQUIRED_LENGTH = 13;
291
292        return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTERS, REQUIRED_LENGTH);
293    }
294
295    /**
296     * Generate a secure random string with is human readable. The resulting string consists of 24 upper case characters
297     * from the Latin alphabet and numbers without '0' and 'O', grouped into 4-characters chunks, e.g.
298     * "TWNK-KD5Y-MT3T-E1GS-DRDB-KVTW". The characters are randomly selected by a cryptographically secure pseudorandom
299     * number generator (CSPRNG).
300     * <p>
301     * The string can be used a backup "code" for secrets, and is in fact the same as the one backup code specified in
302     * XEP-0373 and the one used by the <a href="https://github.com/open-keychain/open-keychain/wiki/Backups">Backup
303     * Format v2 of OpenKeychain</a>.
304     * </p>
305     *
306     * @see <a href="https://xmpp.org/extensions/xep-0373.html#backup-encryption"> XEP-0373 §5.4 Encrypting the Secret
307     *      Key Backup</a>
308     * @return a human readable secure random string.
309     */
310    public static String secureOfflineAttackSafeRandomString() {
311        // 34^24 = 2^122.10 possible combinations. Which is enough to protect against offline brute force attacks.
312        // See also https://www.grc.com/haystack.htm
313        final int REQUIRED_LENGTH = 24;
314
315        return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTERS, REQUIRED_LENGTH);
316    }
317
318    private static final int RANDOM_STRING_CHUNK_SIZE = 4;
319
320    private static String randomString(Random random, char[] alphabet, int numRandomChars) {
321        // The buffer most hold the size of the requested number of random chars and the chunk separators ('-').
322        int bufferSize = numRandomChars + ((numRandomChars - 1) / RANDOM_STRING_CHUNK_SIZE);
323        CharBuffer charBuffer = CharBuffer.allocate(bufferSize);
324
325        try {
326            randomString(charBuffer, random, alphabet, numRandomChars);
327        } catch (IOException e) {
328            // This should never happen if we calculate the buffer size correctly.
329            throw new AssertionError(e);
330        }
331
332        return charBuffer.flip().toString();
333    }
334
335    private static void randomString(Appendable appendable, Random random, char[] alphabet, int numRandomChars)
336                    throws IOException {
337        for (int randomCharNum = 1; randomCharNum <= numRandomChars; randomCharNum++) {
338            int randomIndex = random.nextInt(alphabet.length);
339            char randomChar = alphabet[randomIndex];
340            appendable.append(randomChar);
341
342            if (randomCharNum % RANDOM_STRING_CHUNK_SIZE == 0 && randomCharNum < numRandomChars) {
343                appendable.append('-');
344            }
345        }
346    }
347
348    public static String randomString(final int length) {
349        return randomString(length, RandomUtil.SECURE_RANDOM.get());
350    }
351
352    public static String randomString(final int length, Random random) {
353        if (length == 0) {
354            return "";
355        }
356
357        char[] randomChars = new char[length];
358        for (int i = 0; i < length; i++) {
359            int index = random.nextInt(UNAMBIGUOUS_NUMBERS_AND_LETTERS.length);
360            randomChars[i] = UNAMBIGUOUS_NUMBERS_AND_LETTERS[index];
361        }
362        return new String(randomChars);
363    }
364
365    /**
366     * Returns true if CharSequence is not null and is not empty, false otherwise.
367     * Examples:
368     *    isNotEmpty(null) - false
369     *    isNotEmpty("") - false
370     *    isNotEmpty(" ") - true
371     *    isNotEmpty("empty") - true
372     *
373     * @param cs checked CharSequence
374     * @return true if string is not null and is not empty, false otherwise
375     */
376    public static boolean isNotEmpty(CharSequence cs) {
377        return !isNullOrEmpty(cs);
378    }
379
380    /**
381     * Returns true if the given CharSequence is null or empty.
382     *
383     * @param cs TODO javadoc me please
384     * @return true if the given CharSequence is null or empty
385     */
386    public static boolean isNullOrEmpty(CharSequence cs) {
387        return cs == null || isEmpty(cs);
388    }
389
390    /**
391     * Returns true if all given CharSequences are not empty.
392     *
393     * @param css the CharSequences to test.
394     * @return true if all given CharSequences are not empty.
395     */
396    public static boolean isNotEmpty(CharSequence... css) {
397        for (CharSequence cs : css) {
398            if (StringUtils.isNullOrEmpty(cs)) {
399                return false;
400            }
401        }
402        return true;
403    }
404
405    /**
406     * Returns true if all given CharSequences are either null or empty.
407     *
408     * @param css the CharSequences to test.
409     * @return true if all given CharSequences are null or empty.
410     */
411    public static boolean isNullOrEmpty(CharSequence... css) {
412        for (CharSequence cs : css) {
413            if (StringUtils.isNotEmpty(cs)) {
414                return false;
415            }
416        }
417        return true;
418    }
419
420    public static boolean isNullOrNotEmpty(CharSequence cs) {
421        if (cs == null) {
422            return true;
423        }
424        return !cs.toString().isEmpty();
425    }
426
427    /**
428     * Returns true if the given CharSequence is empty.
429     *
430     * @param cs TODO javadoc me please
431     * @return true if the given CharSequence is empty
432     */
433    public static boolean isEmpty(CharSequence cs) {
434        return cs.length() == 0;
435    }
436
437    /**
438     * Transform a collection of objects to a whitespace delimited String.
439     *
440     * @param collection the collection to transform.
441     * @return a String with all the elements of the collection.
442     */
443    public static String collectionToString(Collection<? extends Object> collection) {
444        return toStringBuilder(collection, " ").toString();
445    }
446
447    /**
448     * Transform a collection of objects to a delimited String.
449     *
450     * @param collection the collection to transform.
451     * @param delimiter the delimiter used to delimit the Strings.
452     * @return a StringBuilder with all the elements of the collection.
453     */
454    public static StringBuilder toStringBuilder(Collection<? extends Object> collection, String delimiter) {
455        StringBuilder sb = new StringBuilder(collection.size() * 20);
456        appendTo(collection, delimiter, sb);
457        return sb;
458    }
459
460    public static void appendTo(Collection<? extends Object> collection, StringBuilder sb) {
461        appendTo(collection, ", ", sb);
462    }
463
464    public static <O extends Object> void appendTo(Collection<O> collection, StringBuilder sb,
465                    Consumer<O> appendFunction) {
466        appendTo(collection, ", ", sb, appendFunction);
467    }
468
469    public static void appendTo(Collection<? extends Object> collection, String delimiter, StringBuilder sb) {
470        appendTo(collection, delimiter, sb, o -> sb.append(o));
471    }
472
473    public static <O extends Object> void appendTo(Collection<O> collection, String delimiter, StringBuilder sb,
474                    Consumer<O> appendFunction) {
475        for (Iterator<O> it = collection.iterator(); it.hasNext();) {
476            O cs = it.next();
477            appendFunction.accept(cs);
478            if (it.hasNext()) {
479                sb.append(delimiter);
480            }
481        }
482    }
483
484    public static String returnIfNotEmptyTrimmed(String string) {
485        if (string == null)
486            return null;
487        String trimmedString = string.trim();
488        if (trimmedString.length() > 0) {
489            return trimmedString;
490        } else {
491            return null;
492        }
493    }
494
495    public static boolean nullSafeCharSequenceEquals(CharSequence csOne, CharSequence csTwo) {
496        return nullSafeCharSequenceComparator(csOne, csTwo) == 0;
497    }
498
499    public static int nullSafeCharSequenceComparator(CharSequence csOne, CharSequence csTwo) {
500        if (csOne == null ^ csTwo == null) {
501            return (csOne == null) ? -1 : 1;
502        }
503        if (csOne == null && csTwo == null) {
504            return 0;
505        }
506        return csOne.toString().compareTo(csTwo.toString());
507    }
508
509    /**
510     * Require a {@link CharSequence} to be neither null, nor empty.
511     *
512     * @deprecated use {@link #requireNotNullNorEmpty(CharSequence, String)} instead.
513     * @param cs CharSequence
514     * @param message error message
515     * @param <CS> CharSequence type
516     * @return cs TODO javadoc me please
517     */
518    @Deprecated
519    public static <CS extends CharSequence> CS requireNotNullOrEmpty(CS cs, String message) {
520        return requireNotNullNorEmpty(cs, message);
521    }
522
523    /**
524     * Require a {@link CharSequence} to be neither null, nor empty.
525     *
526     * @param cs CharSequence
527     * @param message error message
528     * @param <CS> CharSequence type
529     * @return cs TODO javadoc me please
530     */
531    public static <CS extends CharSequence> CS requireNotNullNorEmpty(CS cs, String message) {
532        if (isNullOrEmpty(cs)) {
533            throw new IllegalArgumentException(message);
534        }
535        return cs;
536    }
537
538    public static <CS extends CharSequence> CS requireNullOrNotEmpty(CS cs, String message) {
539        if (cs == null) {
540            return null;
541        }
542        if (isEmpty(cs)) {
543            throw new IllegalArgumentException(message);
544        }
545        return cs;
546    }
547
548    /**
549     * Return the String representation of the given char sequence if it is not null.
550     *
551     * @param cs the char sequence or null.
552     * @return the String representation of <code>cs</code> or null.
553     */
554    public static String maybeToString(CharSequence cs) {
555        if (cs == null) {
556            return null;
557        }
558        return cs.toString();
559    }
560
561    /**
562     * Defined by XML 1.0 § 2.3 as:
563     *  S      ::=      (#x20 | #x9 | #xD | #xA)+
564     *
565     * @see <a href="https://www.w3.org/TR/xml/#sec-white-space">XML 1.0 § 2.3</a>
566     */
567    private static final Pattern XML_WHITESPACE = Pattern.compile("[\t\n\r ]");
568
569    public static String deleteXmlWhitespace(String string) {
570        return XML_WHITESPACE.matcher(string).replaceAll("");
571    }
572
573    public static Appendable appendHeading(Appendable appendable, String heading) throws IOException {
574        return appendHeading(appendable, heading, '-');
575    }
576
577    public static Appendable appendHeading(Appendable appendable, String heading, char underlineChar) throws IOException {
578        appendable.append(heading).append('\n');
579        for (int i = 0; i < heading.length(); i++) {
580            appendable.append(underlineChar);
581        }
582        return appendable.append('\n');
583    }
584
585    public static final String PORTABLE_NEWLINE_REGEX = "\\r?\\n";
586
587    public static List<String> splitLinesPortable(String input) {
588        String[] lines = input.split(PORTABLE_NEWLINE_REGEX);
589        return Arrays.asList(lines);
590    }
591
592    public static List<String> toStrings(Collection<? extends CharSequence> charSequences) {
593        List<String> res = new ArrayList<>(charSequences.size());
594        for (CharSequence cs : charSequences) {
595            String string = cs.toString();
596            res.add(string);
597        }
598        return res;
599    }
600}