You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PathMatcher.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /*
  2. * Copyright (C) 2014, Andrey Loskutov <loskutov@gmx.de>
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.ignore.internal;
  44. import static org.eclipse.jgit.ignore.internal.Strings.checkWildCards;
  45. import static org.eclipse.jgit.ignore.internal.Strings.count;
  46. import static org.eclipse.jgit.ignore.internal.Strings.getPathSeparator;
  47. import static org.eclipse.jgit.ignore.internal.Strings.isWildCard;
  48. import static org.eclipse.jgit.ignore.internal.Strings.split;
  49. import java.util.ArrayList;
  50. import java.util.List;
  51. import org.eclipse.jgit.errors.InvalidPatternException;
  52. import org.eclipse.jgit.ignore.internal.Strings.PatternState;
  53. /**
  54. * Matcher built by patterns consists of multiple path segments.
  55. * <p>
  56. * This class is immutable and thread safe.
  57. */
  58. public class PathMatcher extends AbstractMatcher {
  59. private static final WildMatcher WILD_NO_DIRECTORY = new WildMatcher(false);
  60. private static final WildMatcher WILD_ONLY_DIRECTORY = new WildMatcher(
  61. true);
  62. private final List<IMatcher> matchers;
  63. private final char slash;
  64. private final boolean beginning;
  65. private PathMatcher(String pattern, Character pathSeparator,
  66. boolean dirOnly)
  67. throws InvalidPatternException {
  68. super(pattern, dirOnly);
  69. slash = getPathSeparator(pathSeparator);
  70. beginning = pattern.indexOf(slash) == 0;
  71. if (isSimplePathWithSegments(pattern))
  72. matchers = null;
  73. else
  74. matchers = createMatchers(split(pattern, slash), pathSeparator,
  75. dirOnly);
  76. }
  77. private boolean isSimplePathWithSegments(String path) {
  78. return !isWildCard(path) && path.indexOf('\\') < 0
  79. && count(path, slash, true) > 0;
  80. }
  81. private static List<IMatcher> createMatchers(List<String> segments,
  82. Character pathSeparator, boolean dirOnly)
  83. throws InvalidPatternException {
  84. List<IMatcher> matchers = new ArrayList<>(segments.size());
  85. for (int i = 0; i < segments.size(); i++) {
  86. String segment = segments.get(i);
  87. IMatcher matcher = createNameMatcher0(segment, pathSeparator,
  88. dirOnly, i == segments.size() - 1);
  89. if (i > 0) {
  90. final IMatcher last = matchers.get(matchers.size() - 1);
  91. if (isWild(matcher) && isWild(last))
  92. // collapse wildmatchers **/** is same as **, but preserve
  93. // dirOnly flag (i.e. always use the last wildmatcher)
  94. matchers.remove(matchers.size() - 1);
  95. }
  96. matchers.add(matcher);
  97. }
  98. return matchers;
  99. }
  100. /**
  101. * Create path matcher
  102. *
  103. * @param pattern
  104. * a pattern
  105. * @param pathSeparator
  106. * if this parameter isn't null then this character will not
  107. * match at wildcards(* and ? are wildcards).
  108. * @param dirOnly
  109. * a boolean.
  110. * @return never null
  111. * @throws org.eclipse.jgit.errors.InvalidPatternException
  112. */
  113. public static IMatcher createPathMatcher(String pattern,
  114. Character pathSeparator, boolean dirOnly)
  115. throws InvalidPatternException {
  116. pattern = trim(pattern);
  117. char slash = Strings.getPathSeparator(pathSeparator);
  118. // ignore possible leading and trailing slash
  119. int slashIdx = pattern.indexOf(slash, 1);
  120. if (slashIdx > 0 && slashIdx < pattern.length() - 1)
  121. return new PathMatcher(pattern, pathSeparator, dirOnly);
  122. return createNameMatcher0(pattern, pathSeparator, dirOnly, true);
  123. }
  124. /**
  125. * Trim trailing spaces, unless they are escaped with backslash, see
  126. * https://www.kernel.org/pub/software/scm/git/docs/gitignore.html
  127. *
  128. * @param pattern
  129. * non null
  130. * @return trimmed pattern
  131. */
  132. private static String trim(String pattern) {
  133. while (pattern.length() > 0
  134. && pattern.charAt(pattern.length() - 1) == ' ') {
  135. if (pattern.length() > 1
  136. && pattern.charAt(pattern.length() - 2) == '\\') {
  137. // last space was escaped by backslash: remove backslash and
  138. // keep space
  139. pattern = pattern.substring(0, pattern.length() - 2) + " "; //$NON-NLS-1$
  140. return pattern;
  141. }
  142. pattern = pattern.substring(0, pattern.length() - 1);
  143. }
  144. return pattern;
  145. }
  146. private static IMatcher createNameMatcher0(String segment,
  147. Character pathSeparator, boolean dirOnly, boolean lastSegment)
  148. throws InvalidPatternException {
  149. // check if we see /** or ** segments => double star pattern
  150. if (WildMatcher.WILDMATCH.equals(segment)
  151. || WildMatcher.WILDMATCH2.equals(segment))
  152. return dirOnly && lastSegment ? WILD_ONLY_DIRECTORY
  153. : WILD_NO_DIRECTORY;
  154. PatternState state = checkWildCards(segment);
  155. switch (state) {
  156. case LEADING_ASTERISK_ONLY:
  157. return new LeadingAsteriskMatcher(segment, pathSeparator, dirOnly);
  158. case TRAILING_ASTERISK_ONLY:
  159. return new TrailingAsteriskMatcher(segment, pathSeparator, dirOnly);
  160. case COMPLEX:
  161. return new WildCardMatcher(segment, pathSeparator, dirOnly);
  162. default:
  163. return new NameMatcher(segment, pathSeparator, dirOnly, true);
  164. }
  165. }
  166. /** {@inheritDoc} */
  167. @Override
  168. public boolean matches(String path, boolean assumeDirectory,
  169. boolean pathMatch) {
  170. if (matchers == null) {
  171. return simpleMatch(path, assumeDirectory, pathMatch);
  172. }
  173. return iterate(path, 0, path.length(), assumeDirectory, pathMatch);
  174. }
  175. /*
  176. * Stupid but fast string comparison: the case where we don't have to match
  177. * wildcards or single segments (mean: this is multi-segment path which must
  178. * be at the beginning of the another string)
  179. */
  180. private boolean simpleMatch(String path, boolean assumeDirectory,
  181. boolean pathMatch) {
  182. boolean hasSlash = path.indexOf(slash) == 0;
  183. if (beginning && !hasSlash) {
  184. path = slash + path;
  185. }
  186. if (!beginning && hasSlash) {
  187. path = path.substring(1);
  188. }
  189. if (path.equals(pattern)) {
  190. // Exact match: must meet directory expectations
  191. return !dirOnly || assumeDirectory;
  192. }
  193. /*
  194. * Add slashes for startsWith check. This avoids matching e.g.
  195. * "/src/new" to /src/newfile" but allows "/src/new" to match
  196. * "/src/new/newfile", as is the git standard
  197. */
  198. String prefix = pattern + slash;
  199. if (pathMatch) {
  200. return path.equals(prefix) && (!dirOnly || assumeDirectory);
  201. }
  202. if (path.startsWith(prefix)) {
  203. return true;
  204. }
  205. return false;
  206. }
  207. /** {@inheritDoc} */
  208. @Override
  209. public boolean matches(String segment, int startIncl, int endExcl) {
  210. throw new UnsupportedOperationException(
  211. "Path matcher works only on entire paths"); //$NON-NLS-1$
  212. }
  213. private boolean iterate(final String path, final int startIncl,
  214. final int endExcl, boolean assumeDirectory, boolean pathMatch) {
  215. int matcher = 0;
  216. int right = startIncl;
  217. boolean match = false;
  218. int lastWildmatch = -1;
  219. // ** matches may get extended if a later match fails. When that
  220. // happens, we must extend the ** by exactly one segment.
  221. // wildmatchBacktrackPos records the end of the segment after a **
  222. // match, so that we can reset correctly.
  223. int wildmatchBacktrackPos = -1;
  224. while (true) {
  225. int left = right;
  226. right = path.indexOf(slash, right);
  227. if (right == -1) {
  228. if (left < endExcl) {
  229. match = matches(matcher, path, left, endExcl,
  230. assumeDirectory, pathMatch);
  231. } else {
  232. // a/** should not match a/ or a
  233. match = match && !isWild(matchers.get(matcher));
  234. }
  235. if (match) {
  236. if (matcher < matchers.size() - 1
  237. && isWild(matchers.get(matcher))) {
  238. // ** can match *nothing*: a/**/b match also a/b
  239. matcher++;
  240. match = matches(matcher, path, left, endExcl,
  241. assumeDirectory, pathMatch);
  242. } else if (dirOnly && !assumeDirectory) {
  243. // Directory expectations not met
  244. return false;
  245. }
  246. }
  247. return match && matcher + 1 == matchers.size();
  248. }
  249. if (wildmatchBacktrackPos < 0) {
  250. wildmatchBacktrackPos = right;
  251. }
  252. if (right - left > 0) {
  253. match = matches(matcher, path, left, right, assumeDirectory,
  254. pathMatch);
  255. } else {
  256. // path starts with slash???
  257. right++;
  258. continue;
  259. }
  260. if (match) {
  261. boolean wasWild = isWild(matchers.get(matcher));
  262. if (wasWild) {
  263. lastWildmatch = matcher;
  264. wildmatchBacktrackPos = -1;
  265. // ** can match *nothing*: a/**/b match also a/b
  266. right = left - 1;
  267. }
  268. matcher++;
  269. if (matcher == matchers.size()) {
  270. // We had a prefix match here.
  271. if (!pathMatch) {
  272. return true;
  273. }
  274. if (right == endExcl - 1) {
  275. // Extra slash at the end: actually a full match.
  276. // Must meet directory expectations
  277. return !dirOnly || assumeDirectory;
  278. }
  279. // Prefix matches only if pattern ended with /**
  280. if (wasWild) {
  281. return true;
  282. }
  283. if (lastWildmatch >= 0) {
  284. // Consider pattern **/x and input x/x.
  285. // We've matched the prefix x/ so far: we
  286. // must try to extend the **!
  287. matcher = lastWildmatch + 1;
  288. right = wildmatchBacktrackPos;
  289. wildmatchBacktrackPos = -1;
  290. } else {
  291. return false;
  292. }
  293. }
  294. } else if (lastWildmatch != -1) {
  295. matcher = lastWildmatch + 1;
  296. right = wildmatchBacktrackPos;
  297. wildmatchBacktrackPos = -1;
  298. } else {
  299. return false;
  300. }
  301. right++;
  302. }
  303. }
  304. private boolean matches(int matcherIdx, String path, int startIncl,
  305. int endExcl, boolean assumeDirectory, boolean pathMatch) {
  306. IMatcher matcher = matchers.get(matcherIdx);
  307. final boolean matches = matcher.matches(path, startIncl, endExcl);
  308. if (!matches || !pathMatch || matcherIdx < matchers.size() - 1
  309. || !(matcher instanceof AbstractMatcher)) {
  310. return matches;
  311. }
  312. return assumeDirectory || !((AbstractMatcher) matcher).dirOnly;
  313. }
  314. private static boolean isWild(IMatcher matcher) {
  315. return matcher == WILD_NO_DIRECTORY || matcher == WILD_ONLY_DIRECTORY;
  316. }
  317. }