| /* | |
| * Copyright (C) 2008, Florian Köberle <florianskarten@web.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.fnmatch; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.ListIterator; | |
| import java.util.regex.Matcher; | |
| import java.util.regex.Pattern; | |
| import org.eclipse.jgit.errors.InvalidPatternException; | |
| import org.eclipse.jgit.errors.NoClosingBracketException; | |
| /** | |
| * This class can be used to match filenames against fnmatch like patterns. It | |
| * is not thread save. | |
| * <p> | |
| * Supported are the wildcard characters * and ? and groups with: | |
| * <ul> | |
| * <li>characters e.g. [abc]</li> | |
| * <li>ranges e.g. [a-z]</li> | |
| * <li>the following character classes | |
| * <ul> | |
| * <li>[:alnum:]</li> | |
| * <li>[:alpha:]</li> | |
| * <li>[:blank:]</li> | |
| * <li>[:cntrl:]</li> | |
| * <li>[:digit:]</li> | |
| * <li>[:graph:]</li> | |
| * <li>[:lower:]</li> | |
| * <li>[:print:]</li> | |
| * <li>[:punct:]</li> | |
| * <li>[:space:]</li> | |
| * <li>[:upper:]</li> | |
| * <li>[:word:]</li> | |
| * <li>[:xdigit:]</li> | |
| * </ul> | |
| * e. g. [[:xdigit:]]</li> | |
| * </ul> | |
| * Any character can be escaped by prepending it with a \ | |
| */ | |
| public class FileNameMatcher { | |
| static final List<Head> EMPTY_HEAD_LIST = Collections.emptyList(); | |
| private static final Pattern characterClassStartPattern = Pattern | |
| .compile("\\[[.:=]"); //$NON-NLS-1$ | |
| private List<Head> headsStartValue; | |
| private List<Head> heads; | |
| /** | |
| * {{@link #extendStringToMatchByOneCharacter(char)} needs a list for the | |
| * new heads, allocating a new array would be bad for the performance, as | |
| * the method gets called very often. | |
| * | |
| */ | |
| private List<Head> listForLocalUseage; | |
| /** | |
| * | |
| * @param headsStartValue | |
| * must be a list which will never be modified. | |
| */ | |
| private FileNameMatcher(List<Head> headsStartValue) { | |
| this(headsStartValue, headsStartValue); | |
| } | |
| /** | |
| * | |
| * @param headsStartValue | |
| * must be a list which will never be modified. | |
| * @param heads | |
| * a list which will be cloned and then used as current head | |
| * list. | |
| */ | |
| private FileNameMatcher(final List<Head> headsStartValue, | |
| final List<Head> heads) { | |
| this.headsStartValue = headsStartValue; | |
| this.heads = new ArrayList<>(heads.size()); | |
| this.heads.addAll(heads); | |
| this.listForLocalUseage = new ArrayList<>(heads.size()); | |
| } | |
| /** | |
| * Constructor for FileNameMatcher | |
| * | |
| * @param patternString | |
| * must contain a pattern which fnmatch would accept. | |
| * @param invalidWildgetCharacter | |
| * if this parameter isn't null then this character will not | |
| * match at wildcards(* and ? are wildcards). | |
| * @throws org.eclipse.jgit.errors.InvalidPatternException | |
| * if the patternString contains a invalid fnmatch pattern. | |
| */ | |
| public FileNameMatcher(final String patternString, | |
| final Character invalidWildgetCharacter) | |
| throws InvalidPatternException { | |
| this(createHeadsStartValues(patternString, invalidWildgetCharacter)); | |
| } | |
| /** | |
| * A Copy Constructor which creates a new | |
| * {@link org.eclipse.jgit.fnmatch.FileNameMatcher} with the same state and | |
| * reset point like <code>other</code>. | |
| * | |
| * @param other | |
| * another {@link org.eclipse.jgit.fnmatch.FileNameMatcher} | |
| * instance. | |
| */ | |
| public FileNameMatcher(FileNameMatcher other) { | |
| this(other.headsStartValue, other.heads); | |
| } | |
| private static List<Head> createHeadsStartValues( | |
| final String patternString, final Character invalidWildgetCharacter) | |
| throws InvalidPatternException { | |
| final List<AbstractHead> allHeads = parseHeads(patternString, | |
| invalidWildgetCharacter); | |
| List<Head> nextHeadsSuggestion = new ArrayList<>(2); | |
| nextHeadsSuggestion.add(LastHead.INSTANCE); | |
| for (int i = allHeads.size() - 1; i >= 0; i--) { | |
| final AbstractHead head = allHeads.get(i); | |
| // explanation: | |
| // a and * of the pattern "a*b" | |
| // need *b as newHeads | |
| // that's why * extends the list for it self and it's left neighbor. | |
| if (head.isStar()) { | |
| nextHeadsSuggestion.add(head); | |
| head.setNewHeads(nextHeadsSuggestion); | |
| } else { | |
| head.setNewHeads(nextHeadsSuggestion); | |
| nextHeadsSuggestion = new ArrayList<>(2); | |
| nextHeadsSuggestion.add(head); | |
| } | |
| } | |
| return nextHeadsSuggestion; | |
| } | |
| private static int findGroupEnd(final int indexOfStartBracket, | |
| final String pattern) throws InvalidPatternException { | |
| int firstValidCharClassIndex = indexOfStartBracket + 1; | |
| int firstValidEndBracketIndex = indexOfStartBracket + 2; | |
| if (indexOfStartBracket + 1 >= pattern.length()) | |
| throw new NoClosingBracketException(indexOfStartBracket, "[", "]", //$NON-NLS-1$ //$NON-NLS-2$ | |
| pattern); | |
| if (pattern.charAt(firstValidCharClassIndex) == '!') { | |
| firstValidCharClassIndex++; | |
| firstValidEndBracketIndex++; | |
| } | |
| final Matcher charClassStartMatcher = characterClassStartPattern | |
| .matcher(pattern); | |
| int groupEnd = -1; | |
| while (groupEnd == -1) { | |
| final int possibleGroupEnd = indexOfUnescaped(pattern, ']', | |
| firstValidEndBracketIndex); | |
| if (possibleGroupEnd == -1) | |
| throw new NoClosingBracketException(indexOfStartBracket, "[", //$NON-NLS-1$ | |
| "]", pattern); //$NON-NLS-1$ | |
| final boolean foundCharClass = charClassStartMatcher | |
| .find(firstValidCharClassIndex); | |
| if (foundCharClass | |
| && charClassStartMatcher.start() < possibleGroupEnd) { | |
| final String classStart = charClassStartMatcher.group(0); | |
| final String classEnd = classStart.charAt(1) + "]"; //$NON-NLS-1$ | |
| final int classStartIndex = charClassStartMatcher.start(); | |
| final int classEndIndex = pattern.indexOf(classEnd, | |
| classStartIndex + 2); | |
| if (classEndIndex == -1) | |
| throw new NoClosingBracketException(classStartIndex, | |
| classStart, classEnd, pattern); | |
| firstValidCharClassIndex = classEndIndex + 2; | |
| firstValidEndBracketIndex = firstValidCharClassIndex; | |
| } else { | |
| groupEnd = possibleGroupEnd; | |
| } | |
| } | |
| return groupEnd; | |
| } | |
| private static List<AbstractHead> parseHeads(final String pattern, | |
| final Character invalidWildgetCharacter) | |
| throws InvalidPatternException { | |
| int currentIndex = 0; | |
| List<AbstractHead> heads = new ArrayList<>(); | |
| while (currentIndex < pattern.length()) { | |
| final int groupStart = indexOfUnescaped(pattern, '[', currentIndex); | |
| if (groupStart == -1) { | |
| final String patternPart = pattern.substring(currentIndex); | |
| heads.addAll(createSimpleHeads(patternPart, | |
| invalidWildgetCharacter)); | |
| currentIndex = pattern.length(); | |
| } else { | |
| final String patternPart = pattern.substring(currentIndex, | |
| groupStart); | |
| heads.addAll(createSimpleHeads(patternPart, | |
| invalidWildgetCharacter)); | |
| final int groupEnd = findGroupEnd(groupStart, pattern); | |
| final String groupPart = pattern.substring(groupStart + 1, | |
| groupEnd); | |
| heads.add(new GroupHead(groupPart, pattern)); | |
| currentIndex = groupEnd + 1; | |
| } | |
| } | |
| return heads; | |
| } | |
| private static List<AbstractHead> createSimpleHeads( | |
| final String patternPart, final Character invalidWildgetCharacter) { | |
| final List<AbstractHead> heads = new ArrayList<>( | |
| patternPart.length()); | |
| boolean escaped = false; | |
| for (int i = 0; i < patternPart.length(); i++) { | |
| final char c = patternPart.charAt(i); | |
| if (escaped) { | |
| final CharacterHead head = new CharacterHead(c); | |
| heads.add(head); | |
| escaped = false; | |
| } else { | |
| switch (c) { | |
| case '*': { | |
| final AbstractHead head = createWildCardHead( | |
| invalidWildgetCharacter, true); | |
| heads.add(head); | |
| break; | |
| } | |
| case '?': { | |
| final AbstractHead head = createWildCardHead( | |
| invalidWildgetCharacter, false); | |
| heads.add(head); | |
| break; | |
| } | |
| case '\\': | |
| escaped = true; | |
| break; | |
| default: | |
| final CharacterHead head = new CharacterHead(c); | |
| heads.add(head); | |
| } | |
| } | |
| } | |
| return heads; | |
| } | |
| private static AbstractHead createWildCardHead( | |
| final Character invalidWildgetCharacter, final boolean star) { | |
| if (invalidWildgetCharacter != null) { | |
| return new RestrictedWildCardHead(invalidWildgetCharacter | |
| .charValue(), star); | |
| } | |
| return new WildCardHead(star); | |
| } | |
| /** | |
| * @param c new character to append | |
| * @return true to continue, false if the matcher can stop appending | |
| */ | |
| private boolean extendStringToMatchByOneCharacter(char c) { | |
| final List<Head> newHeads = listForLocalUseage; | |
| newHeads.clear(); | |
| List<Head> lastAddedHeads = null; | |
| for (int i = 0; i < heads.size(); i++) { | |
| final Head head = heads.get(i); | |
| final List<Head> headsToAdd = head.getNextHeads(c); | |
| // Why the next performance optimization isn't wrong: | |
| // Some times two heads return the very same list. | |
| // We save future effort if we don't add these heads again. | |
| // This is the case with the heads "a" and "*" of "a*b" which | |
| // both can return the list ["*","b"] | |
| if (headsToAdd != lastAddedHeads) { | |
| if (!headsToAdd.isEmpty()) | |
| newHeads.addAll(headsToAdd); | |
| lastAddedHeads = headsToAdd; | |
| } | |
| } | |
| listForLocalUseage = heads; | |
| heads = newHeads; | |
| return !newHeads.isEmpty(); | |
| } | |
| private static int indexOfUnescaped(final String searchString, | |
| final char ch, final int fromIndex) { | |
| for (int i = fromIndex; i < searchString.length(); i++) { | |
| char current = searchString.charAt(i); | |
| if (current == ch) | |
| return i; | |
| if (current == '\\') | |
| i++; // Skip the next char as it is escaped } | |
| } | |
| return -1; | |
| } | |
| /** | |
| * Append to the string which is matched against the patterns of this class | |
| * | |
| * @param stringToMatch | |
| * extends the string which is matched against the patterns of | |
| * this class. | |
| */ | |
| public void append(String stringToMatch) { | |
| for (int i = 0; i < stringToMatch.length(); i++) { | |
| final char c = stringToMatch.charAt(i); | |
| if (!extendStringToMatchByOneCharacter(c)) | |
| break; | |
| } | |
| } | |
| /** | |
| * Resets this matcher to it's state right after construction. | |
| */ | |
| public void reset() { | |
| heads.clear(); | |
| heads.addAll(headsStartValue); | |
| } | |
| /** | |
| * Create a {@link org.eclipse.jgit.fnmatch.FileNameMatcher} instance which | |
| * uses the same pattern like this matcher, but has the current state of | |
| * this matcher as reset and start point | |
| * | |
| * @return a {@link org.eclipse.jgit.fnmatch.FileNameMatcher} instance which | |
| * uses the same pattern like this matcher, but has the current | |
| * state of this matcher as reset and start point. | |
| */ | |
| public FileNameMatcher createMatcherForSuffix() { | |
| final List<Head> copyOfHeads = new ArrayList<>(heads.size()); | |
| copyOfHeads.addAll(heads); | |
| return new FileNameMatcher(copyOfHeads); | |
| } | |
| /** | |
| * Whether the matcher matches | |
| * | |
| * @return whether the matcher matches | |
| */ | |
| public boolean isMatch() { | |
| if (heads.isEmpty()) | |
| return false; | |
| final ListIterator<Head> headIterator = heads | |
| .listIterator(heads.size()); | |
| while (headIterator.hasPrevious()) { | |
| final Head head = headIterator.previous(); | |
| if (head == LastHead.INSTANCE) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Whether a match can be appended | |
| * | |
| * @return a boolean. | |
| */ | |
| public boolean canAppendMatch() { | |
| for (Head head : heads) { | |
| if (head != LastHead.INSTANCE) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } |