Strings.java

  1. /*
  2.  * Copyright (C) 2014, 2017 Andrey Loskutov <loskutov@gmx.de> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.ignore.internal;

  11. import static java.lang.Character.isLetter;

  12. import java.text.MessageFormat;
  13. import java.util.ArrayList;
  14. import java.util.Arrays;
  15. import java.util.List;
  16. import java.util.regex.Pattern;
  17. import java.util.regex.PatternSyntaxException;

  18. import org.eclipse.jgit.errors.InvalidPatternException;
  19. import org.eclipse.jgit.ignore.FastIgnoreRule;
  20. import org.eclipse.jgit.internal.JGitText;

  21. /**
  22.  * Various {@link java.lang.String} related utility methods, written mostly to
  23.  * avoid generation of new String objects (e.g. via splitting Strings etc).
  24.  */
  25. public class Strings {

  26.     static char getPathSeparator(Character pathSeparator) {
  27.         return pathSeparator == null ? FastIgnoreRule.PATH_SEPARATOR
  28.                 : pathSeparator.charValue();
  29.     }

  30.     /**
  31.      * Strip trailing characters
  32.      *
  33.      * @param pattern
  34.      *            non null
  35.      * @param c
  36.      *            character to remove
  37.      * @return new string with all trailing characters removed
  38.      */
  39.     public static String stripTrailing(String pattern, char c) {
  40.         for (int i = pattern.length() - 1; i >= 0; i--) {
  41.             char charAt = pattern.charAt(i);
  42.             if (charAt != c) {
  43.                 if (i == pattern.length() - 1) {
  44.                     return pattern;
  45.                 }
  46.                 return pattern.substring(0, i + 1);
  47.             }
  48.         }
  49.         return ""; //$NON-NLS-1$
  50.     }

  51.     /**
  52.      * Strip trailing whitespace characters
  53.      *
  54.      * @param pattern
  55.      *            non null
  56.      * @return new string with all trailing whitespace removed
  57.      */
  58.     public static String stripTrailingWhitespace(String pattern) {
  59.         for (int i = pattern.length() - 1; i >= 0; i--) {
  60.             char charAt = pattern.charAt(i);
  61.             if (!Character.isWhitespace(charAt)) {
  62.                 if (i == pattern.length() - 1) {
  63.                     return pattern;
  64.                 }
  65.                 return pattern.substring(0, i + 1);
  66.             }
  67.         }
  68.         return ""; //$NON-NLS-1$
  69.     }

  70.     /**
  71.      * Check if pattern is a directory pattern ending with a path separator
  72.      *
  73.      * @param pattern
  74.      *            non null
  75.      * @return {@code true} if the last character, which is not whitespace, is a
  76.      *         path separator
  77.      */
  78.     public static boolean isDirectoryPattern(String pattern) {
  79.         for (int i = pattern.length() - 1; i >= 0; i--) {
  80.             char charAt = pattern.charAt(i);
  81.             if (!Character.isWhitespace(charAt)) {
  82.                 return charAt == FastIgnoreRule.PATH_SEPARATOR;
  83.             }
  84.         }
  85.         return false;
  86.     }

  87.     static int count(String s, char c, boolean ignoreFirstLast) {
  88.         int start = 0;
  89.         int count = 0;
  90.         int length = s.length();
  91.         while (start < length) {
  92.             start = s.indexOf(c, start);
  93.             if (start == -1) {
  94.                 break;
  95.             }
  96.             if (!ignoreFirstLast || (start != 0 && start != length - 1)) {
  97.                 count++;
  98.             }
  99.             start++;
  100.         }
  101.         return count;
  102.     }

  103.     /**
  104.      * Splits given string to substrings by given separator
  105.      *
  106.      * @param pattern
  107.      *            non null
  108.      * @param slash
  109.      *            separator char
  110.      * @return list of substrings
  111.      */
  112.     public static List<String> split(String pattern, char slash) {
  113.         int count = count(pattern, slash, true);
  114.         if (count < 1)
  115.             throw new IllegalStateException(
  116.                     "Pattern must have at least two segments: " + pattern); //$NON-NLS-1$
  117.         List<String> segments = new ArrayList<>(count);
  118.         int right = 0;
  119.         while (true) {
  120.             int left = right;
  121.             right = pattern.indexOf(slash, right);
  122.             if (right == -1) {
  123.                 if (left < pattern.length())
  124.                     segments.add(pattern.substring(left));
  125.                 break;
  126.             }
  127.             if (right - left > 0)
  128.                 if (left == 1)
  129.                     // leading slash should remain by the first pattern
  130.                     segments.add(pattern.substring(left - 1, right));
  131.                 else if (right == pattern.length() - 1)
  132.                     // trailing slash should remain too
  133.                     segments.add(pattern.substring(left, right + 1));
  134.                 else
  135.                     segments.add(pattern.substring(left, right));
  136.             right++;
  137.         }
  138.         return segments;
  139.     }

  140.     static boolean isWildCard(String pattern) {
  141.         return pattern.indexOf('*') != -1 || isComplexWildcard(pattern);
  142.     }

  143.     private static boolean isComplexWildcard(String pattern) {
  144.         int idx1 = pattern.indexOf('[');
  145.         if (idx1 != -1) {
  146.             return true;
  147.         }
  148.         if (pattern.indexOf('?') != -1) {
  149.             return true;
  150.         }
  151.         // check if the backslash escapes one of the glob special characters
  152.         // if not, backslash is not part of a regex and treated literally
  153.         int backSlash = pattern.indexOf('\\');
  154.         if (backSlash >= 0) {
  155.             int nextIdx = backSlash + 1;
  156.             if (pattern.length() == nextIdx) {
  157.                 return false;
  158.             }
  159.             char nextChar = pattern.charAt(nextIdx);
  160.             if (escapedByBackslash(nextChar)) {
  161.                 return true;
  162.             }
  163.             return false;
  164.         }
  165.         return false;
  166.     }

  167.     private static boolean escapedByBackslash(char nextChar) {
  168.         return nextChar == '?' || nextChar == '*' || nextChar == '[';
  169.     }

  170.     static PatternState checkWildCards(String pattern) {
  171.         if (isComplexWildcard(pattern))
  172.             return PatternState.COMPLEX;
  173.         int startIdx = pattern.indexOf('*');
  174.         if (startIdx < 0)
  175.             return PatternState.NONE;

  176.         if (startIdx == pattern.length() - 1)
  177.             return PatternState.TRAILING_ASTERISK_ONLY;
  178.         if (pattern.lastIndexOf('*') == 0)
  179.             return PatternState.LEADING_ASTERISK_ONLY;

  180.         return PatternState.COMPLEX;
  181.     }

  182.     enum PatternState {
  183.         LEADING_ASTERISK_ONLY, TRAILING_ASTERISK_ONLY, COMPLEX, NONE
  184.     }

  185.     static final List<String> POSIX_CHAR_CLASSES = Arrays.asList(
  186.             "alnum", "alpha", "blank", "cntrl", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
  187.             // [:alnum:] [:alpha:] [:blank:] [:cntrl:]
  188.             "digit", "graph", "lower", "print", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
  189.             // [:digit:] [:graph:] [:lower:] [:print:]
  190.             "punct", "space", "upper", "xdigit", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
  191.             // [:punct:] [:space:] [:upper:] [:xdigit:]
  192.             "word" //$NON-NLS-1$
  193.     // [:word:] XXX I don't see it in
  194.     // http://man7.org/linux/man-pages/man7/glob.7.html
  195.     // but this was in org.eclipse.jgit.fnmatch.GroupHead.java ???
  196.             );

  197.     private static final String DL = "\\p{javaDigit}\\p{javaLetter}"; //$NON-NLS-1$

  198.     static final List<String> JAVA_CHAR_CLASSES = Arrays
  199.             .asList("\\p{Alnum}", "\\p{javaLetter}", "\\p{Blank}", "\\p{Cntrl}", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
  200.                     // [:alnum:] [:alpha:] [:blank:] [:cntrl:]
  201.                     "\\p{javaDigit}", "[\\p{Graph}" + DL + "]", "\\p{Ll}", "[\\p{Print}" + DL + "]", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
  202.                     // [:digit:] [:graph:] [:lower:] [:print:]
  203.                     "\\p{Punct}", "\\p{Space}", "\\p{Lu}", "\\p{XDigit}", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
  204.                     // [:punct:] [:space:] [:upper:] [:xdigit:]
  205.                     "[" + DL + "_]" //$NON-NLS-1$ //$NON-NLS-2$
  206.                             // [:word:]
  207.             );

  208.     // Collating symbols [[.a.]] or equivalence class expressions [[=a=]] are
  209.     // not supported by CLI git (at least not by 1.9.1)
  210.     static final Pattern UNSUPPORTED = Pattern
  211.             .compile("\\[\\[[.=]\\w+[.=]\\]\\]"); //$NON-NLS-1$

  212.     /**
  213.      * Conversion from glob to Java regex following two sources: <li>
  214.      * http://man7.org/linux/man-pages/man7/glob.7.html <li>
  215.      * org.eclipse.jgit.fnmatch.FileNameMatcher.java Seems that there are
  216.      * various ways to define what "glob" can be.
  217.      *
  218.      * @param pattern
  219.      *            non null pattern
  220.      *
  221.      * @return Java regex pattern corresponding to given glob pattern
  222.      * @throws InvalidPatternException
  223.      */
  224.     static Pattern convertGlob(String pattern) throws InvalidPatternException {
  225.         if (UNSUPPORTED.matcher(pattern).find())
  226.             throw new InvalidPatternException(
  227.                     "Collating symbols [[.a.]] or equivalence class expressions [[=a=]] are not supported", //$NON-NLS-1$
  228.                     pattern);

  229.         StringBuilder sb = new StringBuilder(pattern.length());

  230.         int in_brackets = 0;
  231.         boolean seenEscape = false;
  232.         boolean ignoreLastBracket = false;
  233.         boolean in_char_class = false;
  234.         // 6 is the length of the longest posix char class "xdigit"
  235.         char[] charClass = new char[6];

  236.         for (int i = 0; i < pattern.length(); i++) {
  237.             final char c = pattern.charAt(i);
  238.             switch (c) {

  239.             case '*':
  240.                 if (seenEscape || in_brackets > 0)
  241.                     sb.append(c);
  242.                 else
  243.                     sb.append('.').append(c);
  244.                 break;

  245.             case '(': // fall-through
  246.             case ')': // fall-through
  247.             case '{': // fall-through
  248.             case '}': // fall-through
  249.             case '+': // fall-through
  250.             case '$': // fall-through
  251.             case '^': // fall-through
  252.             case '|':
  253.                 if (seenEscape || in_brackets > 0)
  254.                     sb.append(c);
  255.                 else
  256.                     sb.append('\\').append(c);
  257.                 break;

  258.             case '.':
  259.                 if (seenEscape)
  260.                     sb.append(c);
  261.                 else
  262.                     sb.append('\\').append('.');
  263.                 break;

  264.             case '?':
  265.                 if (seenEscape || in_brackets > 0)
  266.                     sb.append(c);
  267.                 else
  268.                     sb.append('.');
  269.                 break;

  270.             case ':':
  271.                 if (in_brackets > 0)
  272.                     if (lookBehind(sb) == '['
  273.                             && isLetter(lookAhead(pattern, i)))
  274.                         in_char_class = true;
  275.                 sb.append(':');
  276.                 break;

  277.             case '-':
  278.                 if (in_brackets > 0) {
  279.                     if (lookAhead(pattern, i) == ']')
  280.                         sb.append('\\').append(c);
  281.                     else
  282.                         sb.append(c);
  283.                 } else
  284.                     sb.append('-');
  285.                 break;

  286.             case '\\':
  287.                 if (in_brackets > 0) {
  288.                     char lookAhead = lookAhead(pattern, i);
  289.                     if (lookAhead == ']' || lookAhead == '[')
  290.                         ignoreLastBracket = true;
  291.                 } else {
  292.                     //
  293.                     char lookAhead = lookAhead(pattern, i);
  294.                     if (lookAhead != '\\' && lookAhead != '['
  295.                             && lookAhead != '?' && lookAhead != '*'
  296.                             && lookAhead != ' ' && lookBehind(sb) != '\\') {
  297.                         break;
  298.                     }
  299.                 }
  300.                 sb.append(c);
  301.                 break;

  302.             case '[':
  303.                 if (in_brackets > 0) {
  304.                     if (!seenEscape) {
  305.                         sb.append('\\');
  306.                     }
  307.                     sb.append('[');
  308.                     ignoreLastBracket = true;
  309.                 } else {
  310.                     if (!seenEscape) {
  311.                         in_brackets++;
  312.                         ignoreLastBracket = false;
  313.                     }
  314.                     sb.append('[');
  315.                 }
  316.                 break;

  317.             case ']':
  318.                 if (seenEscape) {
  319.                     sb.append(']');
  320.                     ignoreLastBracket = true;
  321.                     break;
  322.                 }
  323.                 if (in_brackets <= 0) {
  324.                     sb.append('\\').append(']');
  325.                     ignoreLastBracket = true;
  326.                     break;
  327.                 }
  328.                 char lookBehind = lookBehind(sb);
  329.                 if ((lookBehind == '[' && !ignoreLastBracket)
  330.                         || lookBehind == '^') {
  331.                     sb.append('\\');
  332.                     sb.append(']');
  333.                     ignoreLastBracket = true;
  334.                 } else {
  335.                     ignoreLastBracket = false;
  336.                     if (!in_char_class) {
  337.                         in_brackets--;
  338.                         sb.append(']');
  339.                     } else {
  340.                         in_char_class = false;
  341.                         String charCl = checkPosixCharClass(charClass);
  342.                         // delete last \[:: chars and set the pattern
  343.                         if (charCl != null) {
  344.                             sb.setLength(sb.length() - 4);
  345.                             sb.append(charCl);
  346.                         }
  347.                         reset(charClass);
  348.                     }
  349.                 }
  350.                 break;

  351.             case '!':
  352.                 if (in_brackets > 0) {
  353.                     if (lookBehind(sb) == '[')
  354.                         sb.append('^');
  355.                     else
  356.                         sb.append(c);
  357.                 } else
  358.                     sb.append(c);
  359.                 break;

  360.             default:
  361.                 if (in_char_class)
  362.                     setNext(charClass, c);
  363.                 else
  364.                     sb.append(c);
  365.                 break;
  366.             } // end switch

  367.             seenEscape = c == '\\';

  368.         } // end for

  369.         if (in_brackets > 0)
  370.             throw new InvalidPatternException("Not closed bracket?", pattern); //$NON-NLS-1$
  371.         try {
  372.             return Pattern.compile(sb.toString(), Pattern.DOTALL);
  373.         } catch (PatternSyntaxException e) {
  374.             throw new InvalidPatternException(
  375.                     MessageFormat.format(JGitText.get().invalidIgnoreRule,
  376.                             pattern),
  377.                     pattern, e);
  378.         }
  379.     }

  380.     /**
  381.      * @param buffer
  382.      * @return zero of the buffer is empty, otherwise the last character from
  383.      *         buffer
  384.      */
  385.     private static char lookBehind(StringBuilder buffer) {
  386.         return buffer.length() > 0 ? buffer.charAt(buffer.length() - 1) : 0;
  387.     }

  388.     /**
  389.      * @param pattern
  390.      * @param i
  391.      *            current pointer in the pattern
  392.      * @return zero of the index is out of range, otherwise the next character
  393.      *         from given position
  394.      */
  395.     private static char lookAhead(String pattern, int i) {
  396.         int idx = i + 1;
  397.         return idx >= pattern.length() ? 0 : pattern.charAt(idx);
  398.     }

  399.     private static void setNext(char[] buffer, char c) {
  400.         for (int i = 0; i < buffer.length; i++)
  401.             if (buffer[i] == 0) {
  402.                 buffer[i] = c;
  403.                 break;
  404.             }
  405.     }

  406.     private static void reset(char[] buffer) {
  407.         for (int i = 0; i < buffer.length; i++)
  408.             buffer[i] = 0;
  409.     }

  410.     private static String checkPosixCharClass(char[] buffer) {
  411.         for (int i = 0; i < POSIX_CHAR_CLASSES.size(); i++) {
  412.             String clazz = POSIX_CHAR_CLASSES.get(i);
  413.             boolean match = true;
  414.             for (int j = 0; j < clazz.length(); j++)
  415.                 if (buffer[j] != clazz.charAt(j)) {
  416.                     match = false;
  417.                     break;
  418.                 }
  419.             if (match)
  420.                 return JAVA_CHAR_CLASSES.get(i);
  421.         }
  422.         return null;
  423.     }

  424.     static String deleteBackslash(String s) {
  425.         if (s.indexOf('\\') < 0) {
  426.             return s;
  427.         }
  428.         StringBuilder sb = new StringBuilder(s.length());
  429.         for (int i = 0; i < s.length(); i++) {
  430.             char ch = s.charAt(i);
  431.             if (ch == '\\') {
  432.                 if (i + 1 == s.length()) {
  433.                     continue;
  434.                 }
  435.                 char next = s.charAt(i + 1);
  436.                 if (next == '\\') {
  437.                     sb.append(ch);
  438.                     i++;
  439.                     continue;
  440.                 }
  441.                 if (!escapedByBackslash(next)) {
  442.                     continue;
  443.                 }
  444.             }
  445.             sb.append(ch);
  446.         }
  447.         return sb.toString();
  448.     }

  449. }