PathMatcher.java
- /*
- * Copyright (C) 2014, Andrey Loskutov <loskutov@gmx.de> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.ignore.internal;
- import static org.eclipse.jgit.ignore.internal.Strings.checkWildCards;
- import static org.eclipse.jgit.ignore.internal.Strings.count;
- import static org.eclipse.jgit.ignore.internal.Strings.getPathSeparator;
- import static org.eclipse.jgit.ignore.internal.Strings.isWildCard;
- import static org.eclipse.jgit.ignore.internal.Strings.split;
- import java.util.ArrayList;
- import java.util.List;
- import org.eclipse.jgit.errors.InvalidPatternException;
- import org.eclipse.jgit.ignore.IMatcher;
- import org.eclipse.jgit.ignore.internal.Strings.PatternState;
- /**
- * Matcher built by patterns consists of multiple path segments.
- * <p>
- * This class is immutable and thread safe.
- */
- public class PathMatcher extends AbstractMatcher {
- private static final WildMatcher WILD_NO_DIRECTORY = new WildMatcher(false);
- private static final WildMatcher WILD_ONLY_DIRECTORY = new WildMatcher(
- true);
- private final List<IMatcher> matchers;
- private final char slash;
- private final boolean beginning;
- private PathMatcher(String pattern, Character pathSeparator,
- boolean dirOnly)
- throws InvalidPatternException {
- super(pattern, dirOnly);
- slash = getPathSeparator(pathSeparator);
- beginning = pattern.indexOf(slash) == 0;
- if (isSimplePathWithSegments(pattern))
- matchers = null;
- else
- matchers = createMatchers(split(pattern, slash), pathSeparator,
- dirOnly);
- }
- private boolean isSimplePathWithSegments(String path) {
- return !isWildCard(path) && path.indexOf('\\') < 0
- && count(path, slash, true) > 0;
- }
- private static List<IMatcher> createMatchers(List<String> segments,
- Character pathSeparator, boolean dirOnly)
- throws InvalidPatternException {
- List<IMatcher> matchers = new ArrayList<>(segments.size());
- for (int i = 0; i < segments.size(); i++) {
- String segment = segments.get(i);
- IMatcher matcher = createNameMatcher0(segment, pathSeparator,
- dirOnly, i == segments.size() - 1);
- if (i > 0) {
- final IMatcher last = matchers.get(matchers.size() - 1);
- if (isWild(matcher) && isWild(last))
- // collapse wildmatchers **/** is same as **, but preserve
- // dirOnly flag (i.e. always use the last wildmatcher)
- matchers.remove(matchers.size() - 1);
- }
- matchers.add(matcher);
- }
- return matchers;
- }
- /**
- * Create path matcher
- *
- * @param pattern
- * a pattern
- * @param pathSeparator
- * if this parameter isn't null then this character will not
- * match at wildcards(* and ? are wildcards).
- * @param dirOnly
- * a boolean.
- * @return never null
- * @throws org.eclipse.jgit.errors.InvalidPatternException
- */
- public static IMatcher createPathMatcher(String pattern,
- Character pathSeparator, boolean dirOnly)
- throws InvalidPatternException {
- pattern = trim(pattern);
- char slash = Strings.getPathSeparator(pathSeparator);
- // ignore possible leading and trailing slash
- int slashIdx = pattern.indexOf(slash, 1);
- if (slashIdx > 0 && slashIdx < pattern.length() - 1)
- return new PathMatcher(pattern, pathSeparator, dirOnly);
- return createNameMatcher0(pattern, pathSeparator, dirOnly, true);
- }
- /**
- * Trim trailing spaces, unless they are escaped with backslash, see
- * https://www.kernel.org/pub/software/scm/git/docs/gitignore.html
- *
- * @param pattern
- * non null
- * @return trimmed pattern
- */
- private static String trim(String pattern) {
- while (pattern.length() > 0
- && pattern.charAt(pattern.length() - 1) == ' ') {
- if (pattern.length() > 1
- && pattern.charAt(pattern.length() - 2) == '\\') {
- // last space was escaped by backslash: remove backslash and
- // keep space
- pattern = pattern.substring(0, pattern.length() - 2) + " "; //$NON-NLS-1$
- return pattern;
- }
- pattern = pattern.substring(0, pattern.length() - 1);
- }
- return pattern;
- }
- private static IMatcher createNameMatcher0(String segment,
- Character pathSeparator, boolean dirOnly, boolean lastSegment)
- throws InvalidPatternException {
- // check if we see /** or ** segments => double star pattern
- if (WildMatcher.WILDMATCH.equals(segment)
- || WildMatcher.WILDMATCH2.equals(segment))
- return dirOnly && lastSegment ? WILD_ONLY_DIRECTORY
- : WILD_NO_DIRECTORY;
- PatternState state = checkWildCards(segment);
- switch (state) {
- case LEADING_ASTERISK_ONLY:
- return new LeadingAsteriskMatcher(segment, pathSeparator, dirOnly);
- case TRAILING_ASTERISK_ONLY:
- return new TrailingAsteriskMatcher(segment, pathSeparator, dirOnly);
- case COMPLEX:
- return new WildCardMatcher(segment, pathSeparator, dirOnly);
- default:
- return new NameMatcher(segment, pathSeparator, dirOnly, true);
- }
- }
- /** {@inheritDoc} */
- @Override
- public boolean matches(String path, boolean assumeDirectory,
- boolean pathMatch) {
- if (matchers == null) {
- return simpleMatch(path, assumeDirectory, pathMatch);
- }
- return iterate(path, 0, path.length(), assumeDirectory, pathMatch);
- }
- /*
- * Stupid but fast string comparison: the case where we don't have to match
- * wildcards or single segments (mean: this is multi-segment path which must
- * be at the beginning of the another string)
- */
- private boolean simpleMatch(String path, boolean assumeDirectory,
- boolean pathMatch) {
- boolean hasSlash = path.indexOf(slash) == 0;
- if (beginning && !hasSlash) {
- path = slash + path;
- }
- if (!beginning && hasSlash) {
- path = path.substring(1);
- }
- if (path.equals(pattern)) {
- // Exact match: must meet directory expectations
- return !dirOnly || assumeDirectory;
- }
- /*
- * Add slashes for startsWith check. This avoids matching e.g.
- * "/src/new" to /src/newfile" but allows "/src/new" to match
- * "/src/new/newfile", as is the git standard
- */
- String prefix = pattern + slash;
- if (pathMatch) {
- return path.equals(prefix) && (!dirOnly || assumeDirectory);
- }
- if (path.startsWith(prefix)) {
- return true;
- }
- return false;
- }
- /** {@inheritDoc} */
- @Override
- public boolean matches(String segment, int startIncl, int endExcl) {
- throw new UnsupportedOperationException(
- "Path matcher works only on entire paths"); //$NON-NLS-1$
- }
- private boolean iterate(final String path, final int startIncl,
- final int endExcl, boolean assumeDirectory, boolean pathMatch) {
- int matcher = 0;
- int right = startIncl;
- boolean match = false;
- int lastWildmatch = -1;
- // ** matches may get extended if a later match fails. When that
- // happens, we must extend the ** by exactly one segment.
- // wildmatchBacktrackPos records the end of the segment after a **
- // match, so that we can reset correctly.
- int wildmatchBacktrackPos = -1;
- while (true) {
- int left = right;
- right = path.indexOf(slash, right);
- if (right == -1) {
- if (left < endExcl) {
- match = matches(matcher, path, left, endExcl,
- assumeDirectory, pathMatch);
- } else {
- // a/** should not match a/ or a
- match = match && !isWild(matchers.get(matcher));
- }
- if (match) {
- if (matcher < matchers.size() - 1
- && isWild(matchers.get(matcher))) {
- // ** can match *nothing*: a/**/b match also a/b
- matcher++;
- match = matches(matcher, path, left, endExcl,
- assumeDirectory, pathMatch);
- } else if (dirOnly && !assumeDirectory) {
- // Directory expectations not met
- return false;
- }
- }
- return match && matcher + 1 == matchers.size();
- }
- if (wildmatchBacktrackPos < 0) {
- wildmatchBacktrackPos = right;
- }
- if (right - left > 0) {
- match = matches(matcher, path, left, right, assumeDirectory,
- pathMatch);
- } else {
- // path starts with slash???
- right++;
- continue;
- }
- if (match) {
- boolean wasWild = isWild(matchers.get(matcher));
- if (wasWild) {
- lastWildmatch = matcher;
- wildmatchBacktrackPos = -1;
- // ** can match *nothing*: a/**/b match also a/b
- right = left - 1;
- }
- matcher++;
- if (matcher == matchers.size()) {
- // We had a prefix match here.
- if (!pathMatch) {
- return true;
- }
- if (right == endExcl - 1) {
- // Extra slash at the end: actually a full match.
- // Must meet directory expectations
- return !dirOnly || assumeDirectory;
- }
- // Prefix matches only if pattern ended with /**
- if (wasWild) {
- return true;
- }
- if (lastWildmatch >= 0) {
- // Consider pattern **/x and input x/x.
- // We've matched the prefix x/ so far: we
- // must try to extend the **!
- matcher = lastWildmatch + 1;
- right = wildmatchBacktrackPos;
- wildmatchBacktrackPos = -1;
- } else {
- return false;
- }
- }
- } else if (lastWildmatch != -1) {
- matcher = lastWildmatch + 1;
- right = wildmatchBacktrackPos;
- wildmatchBacktrackPos = -1;
- } else {
- return false;
- }
- right++;
- }
- }
- private boolean matches(int matcherIdx, String path, int startIncl,
- int endExcl, boolean assumeDirectory, boolean pathMatch) {
- IMatcher matcher = matchers.get(matcherIdx);
- final boolean matches = matcher.matches(path, startIncl, endExcl);
- if (!matches || !pathMatch || matcherIdx < matchers.size() - 1
- || !(matcher instanceof AbstractMatcher)) {
- return matches;
- }
- return assumeDirectory || !((AbstractMatcher) matcher).dirOnly;
- }
- private static boolean isWild(IMatcher matcher) {
- return matcher == WILD_NO_DIRECTORY || matcher == WILD_ONLY_DIRECTORY;
- }
- }