PathMatcher.java

  1. /*
  2.  * Copyright (C) 2014, 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 org.eclipse.jgit.ignore.internal.Strings.checkWildCards;
  12. import static org.eclipse.jgit.ignore.internal.Strings.count;
  13. import static org.eclipse.jgit.ignore.internal.Strings.getPathSeparator;
  14. import static org.eclipse.jgit.ignore.internal.Strings.isWildCard;
  15. import static org.eclipse.jgit.ignore.internal.Strings.split;

  16. import java.util.ArrayList;
  17. import java.util.List;

  18. import org.eclipse.jgit.errors.InvalidPatternException;
  19. import org.eclipse.jgit.ignore.IMatcher;
  20. import org.eclipse.jgit.ignore.internal.Strings.PatternState;

  21. /**
  22.  * Matcher built by patterns consists of multiple path segments.
  23.  * <p>
  24.  * This class is immutable and thread safe.
  25.  */
  26. public class PathMatcher extends AbstractMatcher {

  27.     private static final WildMatcher WILD_NO_DIRECTORY = new WildMatcher(false);

  28.     private static final WildMatcher WILD_ONLY_DIRECTORY = new WildMatcher(
  29.             true);

  30.     private final List<IMatcher> matchers;

  31.     private final char slash;

  32.     private final boolean beginning;

  33.     private PathMatcher(String pattern, Character pathSeparator,
  34.             boolean dirOnly)
  35.             throws InvalidPatternException {
  36.         super(pattern, dirOnly);
  37.         slash = getPathSeparator(pathSeparator);
  38.         beginning = pattern.indexOf(slash) == 0;
  39.         if (isSimplePathWithSegments(pattern))
  40.             matchers = null;
  41.         else
  42.             matchers = createMatchers(split(pattern, slash), pathSeparator,
  43.                     dirOnly);
  44.     }

  45.     private boolean isSimplePathWithSegments(String path) {
  46.         return !isWildCard(path) && path.indexOf('\\') < 0
  47.                 && count(path, slash, true) > 0;
  48.     }

  49.     private static List<IMatcher> createMatchers(List<String> segments,
  50.             Character pathSeparator, boolean dirOnly)
  51.             throws InvalidPatternException {
  52.         List<IMatcher> matchers = new ArrayList<>(segments.size());
  53.         for (int i = 0; i < segments.size(); i++) {
  54.             String segment = segments.get(i);
  55.             IMatcher matcher = createNameMatcher0(segment, pathSeparator,
  56.                     dirOnly, i == segments.size() - 1);
  57.             if (i > 0) {
  58.                 final IMatcher last = matchers.get(matchers.size() - 1);
  59.                 if (isWild(matcher) && isWild(last))
  60.                     // collapse wildmatchers **/** is same as **, but preserve
  61.                     // dirOnly flag (i.e. always use the last wildmatcher)
  62.                     matchers.remove(matchers.size() - 1);
  63.             }

  64.             matchers.add(matcher);
  65.         }
  66.         return matchers;
  67.     }

  68.     /**
  69.      * Create path matcher
  70.      *
  71.      * @param pattern
  72.      *            a pattern
  73.      * @param pathSeparator
  74.      *            if this parameter isn't null then this character will not
  75.      *            match at wildcards(* and ? are wildcards).
  76.      * @param dirOnly
  77.      *            a boolean.
  78.      * @return never null
  79.      * @throws org.eclipse.jgit.errors.InvalidPatternException
  80.      */
  81.     public static IMatcher createPathMatcher(String pattern,
  82.             Character pathSeparator, boolean dirOnly)
  83.             throws InvalidPatternException {
  84.         pattern = trim(pattern);
  85.         char slash = Strings.getPathSeparator(pathSeparator);
  86.         // ignore possible leading and trailing slash
  87.         int slashIdx = pattern.indexOf(slash, 1);
  88.         if (slashIdx > 0 && slashIdx < pattern.length() - 1)
  89.             return new PathMatcher(pattern, pathSeparator, dirOnly);
  90.         return createNameMatcher0(pattern, pathSeparator, dirOnly, true);
  91.     }

  92.     /**
  93.      * Trim trailing spaces, unless they are escaped with backslash, see
  94.      * https://www.kernel.org/pub/software/scm/git/docs/gitignore.html
  95.      *
  96.      * @param pattern
  97.      *            non null
  98.      * @return trimmed pattern
  99.      */
  100.     private static String trim(String pattern) {
  101.         while (pattern.length() > 0
  102.                 && pattern.charAt(pattern.length() - 1) == ' ') {
  103.             if (pattern.length() > 1
  104.                     && pattern.charAt(pattern.length() - 2) == '\\') {
  105.                 // last space was escaped by backslash: remove backslash and
  106.                 // keep space
  107.                 pattern = pattern.substring(0, pattern.length() - 2) + " "; //$NON-NLS-1$
  108.                 return pattern;
  109.             }
  110.             pattern = pattern.substring(0, pattern.length() - 1);
  111.         }
  112.         return pattern;
  113.     }

  114.     private static IMatcher createNameMatcher0(String segment,
  115.             Character pathSeparator, boolean dirOnly, boolean lastSegment)
  116.             throws InvalidPatternException {
  117.         // check if we see /** or ** segments => double star pattern
  118.         if (WildMatcher.WILDMATCH.equals(segment)
  119.                 || WildMatcher.WILDMATCH2.equals(segment))
  120.             return dirOnly && lastSegment ? WILD_ONLY_DIRECTORY
  121.                     : WILD_NO_DIRECTORY;

  122.         PatternState state = checkWildCards(segment);
  123.         switch (state) {
  124.         case LEADING_ASTERISK_ONLY:
  125.             return new LeadingAsteriskMatcher(segment, pathSeparator, dirOnly);
  126.         case TRAILING_ASTERISK_ONLY:
  127.             return new TrailingAsteriskMatcher(segment, pathSeparator, dirOnly);
  128.         case COMPLEX:
  129.             return new WildCardMatcher(segment, pathSeparator, dirOnly);
  130.         default:
  131.             return new NameMatcher(segment, pathSeparator, dirOnly, true);
  132.         }
  133.     }

  134.     /** {@inheritDoc} */
  135.     @Override
  136.     public boolean matches(String path, boolean assumeDirectory,
  137.             boolean pathMatch) {
  138.         if (matchers == null) {
  139.             return simpleMatch(path, assumeDirectory, pathMatch);
  140.         }
  141.         return iterate(path, 0, path.length(), assumeDirectory, pathMatch);
  142.     }

  143.     /*
  144.      * Stupid but fast string comparison: the case where we don't have to match
  145.      * wildcards or single segments (mean: this is multi-segment path which must
  146.      * be at the beginning of the another string)
  147.      */
  148.     private boolean simpleMatch(String path, boolean assumeDirectory,
  149.             boolean pathMatch) {
  150.         boolean hasSlash = path.indexOf(slash) == 0;
  151.         if (beginning && !hasSlash) {
  152.             path = slash + path;
  153.         }
  154.         if (!beginning && hasSlash) {
  155.             path = path.substring(1);
  156.         }
  157.         if (path.equals(pattern)) {
  158.             // Exact match: must meet directory expectations
  159.             return !dirOnly || assumeDirectory;
  160.         }
  161.         /*
  162.          * Add slashes for startsWith check. This avoids matching e.g.
  163.          * "/src/new" to /src/newfile" but allows "/src/new" to match
  164.          * "/src/new/newfile", as is the git standard
  165.          */
  166.         String prefix = pattern + slash;
  167.         if (pathMatch) {
  168.             return path.equals(prefix) && (!dirOnly || assumeDirectory);
  169.         }
  170.         if (path.startsWith(prefix)) {
  171.             return true;
  172.         }
  173.         return false;
  174.     }

  175.     /** {@inheritDoc} */
  176.     @Override
  177.     public boolean matches(String segment, int startIncl, int endExcl) {
  178.         throw new UnsupportedOperationException(
  179.                 "Path matcher works only on entire paths"); //$NON-NLS-1$
  180.     }

  181.     private boolean iterate(final String path, final int startIncl,
  182.             final int endExcl, boolean assumeDirectory, boolean pathMatch) {
  183.         int matcher = 0;
  184.         int right = startIncl;
  185.         boolean match = false;
  186.         int lastWildmatch = -1;
  187.         // ** matches may get extended if a later match fails. When that
  188.         // happens, we must extend the ** by exactly one segment.
  189.         // wildmatchBacktrackPos records the end of the segment after a **
  190.         // match, so that we can reset correctly.
  191.         int wildmatchBacktrackPos = -1;
  192.         while (true) {
  193.             int left = right;
  194.             right = path.indexOf(slash, right);
  195.             if (right == -1) {
  196.                 if (left < endExcl) {
  197.                     match = matches(matcher, path, left, endExcl,
  198.                             assumeDirectory, pathMatch);
  199.                 } else {
  200.                     // a/** should not match a/ or a
  201.                     match = match && !isWild(matchers.get(matcher));
  202.                 }
  203.                 if (match) {
  204.                     if (matcher < matchers.size() - 1
  205.                             && isWild(matchers.get(matcher))) {
  206.                         // ** can match *nothing*: a/**/b match also a/b
  207.                         matcher++;
  208.                         match = matches(matcher, path, left, endExcl,
  209.                                 assumeDirectory, pathMatch);
  210.                     } else if (dirOnly && !assumeDirectory) {
  211.                         // Directory expectations not met
  212.                         return false;
  213.                     }
  214.                 }
  215.                 return match && matcher + 1 == matchers.size();
  216.             }
  217.             if (wildmatchBacktrackPos < 0) {
  218.                 wildmatchBacktrackPos = right;
  219.             }
  220.             if (right - left > 0) {
  221.                 match = matches(matcher, path, left, right, assumeDirectory,
  222.                         pathMatch);
  223.             } else {
  224.                 // path starts with slash???
  225.                 right++;
  226.                 continue;
  227.             }
  228.             if (match) {
  229.                 boolean wasWild = isWild(matchers.get(matcher));
  230.                 if (wasWild) {
  231.                     lastWildmatch = matcher;
  232.                     wildmatchBacktrackPos = -1;
  233.                     // ** can match *nothing*: a/**/b match also a/b
  234.                     right = left - 1;
  235.                 }
  236.                 matcher++;
  237.                 if (matcher == matchers.size()) {
  238.                     // We had a prefix match here.
  239.                     if (!pathMatch) {
  240.                         return true;
  241.                     }
  242.                     if (right == endExcl - 1) {
  243.                         // Extra slash at the end: actually a full match.
  244.                         // Must meet directory expectations
  245.                         return !dirOnly || assumeDirectory;
  246.                     }
  247.                     // Prefix matches only if pattern ended with /**
  248.                     if (wasWild) {
  249.                         return true;
  250.                     }
  251.                     if (lastWildmatch >= 0) {
  252.                         // Consider pattern **/x and input x/x.
  253.                         // We've matched the prefix x/ so far: we
  254.                         // must try to extend the **!
  255.                         matcher = lastWildmatch + 1;
  256.                         right = wildmatchBacktrackPos;
  257.                         wildmatchBacktrackPos = -1;
  258.                     } else {
  259.                         return false;
  260.                     }
  261.                 }
  262.             } else if (lastWildmatch != -1) {
  263.                 matcher = lastWildmatch + 1;
  264.                 right = wildmatchBacktrackPos;
  265.                 wildmatchBacktrackPos = -1;
  266.             } else {
  267.                 return false;
  268.             }
  269.             right++;
  270.         }
  271.     }

  272.     private boolean matches(int matcherIdx, String path, int startIncl,
  273.             int endExcl, boolean assumeDirectory, boolean pathMatch) {
  274.         IMatcher matcher = matchers.get(matcherIdx);

  275.         final boolean matches = matcher.matches(path, startIncl, endExcl);
  276.         if (!matches || !pathMatch || matcherIdx < matchers.size() - 1
  277.                 || !(matcher instanceof AbstractMatcher)) {
  278.             return matches;
  279.         }

  280.         return assumeDirectory || !((AbstractMatcher) matcher).dirOnly;
  281.     }

  282.     private static boolean isWild(IMatcher matcher) {
  283.         return matcher == WILD_NO_DIRECTORY || matcher == WILD_ONLY_DIRECTORY;
  284.     }
  285. }