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;
}
}