/*
* Copyright 2015 floragunn UG (haftungsbeschränkt)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.floragunn.searchguard.support;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;
public class WildcardMatcher {
private static final int NOT_FOUND = -1;
public static boolean matchAny(final String[] pattern, final String[] candidate) {
for (int i = 0; i < pattern.length; i++) {
final String string = pattern[i];
if (matchAny(string, candidate)) {
return true;
}
}
return false;
}
public static boolean matchAll(final String[] pattern, final String[] candidate) {
for (int i = 0; i < candidate.length; i++) {
final String string = candidate[i];
if (!matchAny(pattern, string)) {
return false;
}
}
return true;
}
public static boolean allPatternsMatched(final String[] pattern, final String[] candidate) {
int matchedPatternNum = 0;
for (int i = 0; i < pattern.length; i++) {
final String string = pattern[i];
if (matchAny(string, candidate)) {
matchedPatternNum++;
}
}
return matchedPatternNum == pattern.length && pattern.length > 0;
}
public static boolean matchAny(final String pattern, final String[] candidate) {
for (int i = 0; i < candidate.length; i++) {
final String string = candidate[i];
if (match(pattern, string)) {
return true;
}
}
return false;
}
public static List<String> getMatchAny(final String pattern, final String[] candidate) {
final List<String> matches = new ArrayList<String>(candidate.length);
for (int i = 0; i < candidate.length; i++) {
final String string = candidate[i];
if (match(pattern, string)) {
matches.add(string);
}
}
return matches;
}
public static boolean matchAny(final String pattern[], final String candidate) {
for (int i = 0; i < pattern.length; i++) {
final String string = pattern[i];
if (match(string, candidate)) {
return true;
}
}
return false;
}
public static boolean match(final String pattern, final String candidate) {
if (pattern == null || candidate == null) {
return false;
}
if (pattern.startsWith("/") && pattern.endsWith("/")) {
// regex
return Pattern.matches("^"+pattern.substring(1, pattern.length() - 1)+"$", candidate);
} else if (pattern.length() == 1 && pattern.charAt(0) == '*') {
return true;
} else if (pattern.indexOf('?') == NOT_FOUND && pattern.indexOf('*') == NOT_FOUND) {
return pattern.equals(candidate);
} else {
return simpleWildcardMatch(pattern, candidate);
}
}
public static boolean containsWildcard(final String pattern) {
if (pattern != null
&& (pattern.indexOf("*") > NOT_FOUND || pattern.indexOf("?") > NOT_FOUND || (pattern.startsWith("/") && pattern
.endsWith("/")))) {
return true;
}
return false;
}
//All code below is copied (and slightly modified) from Apache Commons IO
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Checks a filename to see if it matches the specified wildcard matcher
* allowing control over case-sensitivity.
* <p>
* The wildcard matcher uses the characters '?' and '*' to represent a
* single or multiple (zero or more) wildcard characters.
* N.B. the sequence "*?" does not work properly at present in match strings.
*
* @param candidate the filename to match on
* @param pattern the wildcard string to match against
* @return true if the filename matches the wilcard string
* @since 1.3
*/
private static boolean simpleWildcardMatch(final String pattern, final String candidate) {
if (candidate == null && pattern == null) {
return true;
}
if (candidate == null || pattern == null) {
return false;
}
final String[] wcs = splitOnTokens(pattern);
boolean anyChars = false;
int textIdx = 0;
int wcsIdx = 0;
final Stack<int[]> backtrack = new Stack<>();
// loop around a backtrack stack, to handle complex * matching
do {
if (backtrack.size() > 0) {
final int[] array = backtrack.pop();
wcsIdx = array[0];
textIdx = array[1];
anyChars = true;
}
// loop whilst tokens and text left to process
while (wcsIdx < wcs.length) {
if (wcs[wcsIdx].equals("?")) {
// ? so move to next text char
textIdx++;
if (textIdx > candidate.length()) {
break;
}
anyChars = false;
} else if (wcs[wcsIdx].equals("*")) {
// set any chars status
anyChars = true;
if (wcsIdx == wcs.length - 1) {
textIdx = candidate.length();
}
} else {
// matching text token
if (anyChars) {
// any chars then try to locate text token
textIdx = checkIndexOf(candidate, textIdx, wcs[wcsIdx]);
if (textIdx == NOT_FOUND) {
// token not found
break;
}
final int repeat = checkIndexOf(candidate, textIdx + 1, wcs[wcsIdx]);
if (repeat >= 0) {
backtrack.push(new int[] {wcsIdx, repeat});
}
} else {
// matching from current position
if (!checkRegionMatches(candidate, textIdx, wcs[wcsIdx])) {
// couldnt match token
break;
}
}
// matched text token, move text index to end of matched token
textIdx += wcs[wcsIdx].length();
anyChars = false;
}
wcsIdx++;
}
// full match
if (wcsIdx == wcs.length && textIdx == candidate.length()) {
return true;
}
} while (backtrack.size() > 0);
return false;
}
/**
* Splits a string into a number of tokens.
* The text is split by '?' and '*'.
* Where multiple '*' occur consecutively they are collapsed into a single '*'.
*
* @param text the text to split
* @return the array of tokens, never null
*/
private static String[] splitOnTokens(final String text) {
// used by wildcardMatch
// package level so a unit test may run on this
if (text.indexOf('?') == NOT_FOUND && text.indexOf('*') == NOT_FOUND) {
return new String[] { text };
}
final char[] array = text.toCharArray();
final ArrayList<String> list = new ArrayList<>();
final StringBuilder buffer = new StringBuilder();
char prevChar = 0;
for (final char ch : array) {
if (ch == '?' || ch == '*') {
if (buffer.length() != 0) {
list.add(buffer.toString());
buffer.setLength(0);
}
if (ch == '?') {
list.add("?");
} else if (prevChar != '*') {// ch == '*' here; check if previous char was '*'
list.add("*");
}
} else {
buffer.append(ch);
}
prevChar = ch;
}
if (buffer.length() != 0) {
list.add(buffer.toString());
}
return list.toArray( new String[ list.size() ] );
}
/**
* Checks if one string contains another starting at a specific index using the
* case-sensitivity rule.
* <p>
* This method mimics parts of {@link String#indexOf(String, int)}
* but takes case-sensitivity into account.
*
* @param str the string to check, not null
* @param strStartIndex the index to start at in str
* @param search the start to search for, not null
* @return the first index of the search String,
* -1 if no match or {@code null} string input
* @throws NullPointerException if either string is null
* @since 2.0
*/
private static int checkIndexOf(final String str, final int strStartIndex, final String search) {
final int endIndex = str.length() - search.length();
if (endIndex >= strStartIndex) {
for (int i = strStartIndex; i <= endIndex; i++) {
if (checkRegionMatches(str, i, search)) {
return i;
}
}
}
return -1;
}
/**
* Checks if one string contains another at a specific index using the case-sensitivity rule.
* <p>
* This method mimics parts of {@link String#regionMatches(boolean, int, String, int, int)}
* but takes case-sensitivity into account.
*
* @param str the string to check, not null
* @param strStartIndex the index to start at in str
* @param search the start to search for, not null
* @return true if equal using the case rules
* @throws NullPointerException if either string is null
*/
private static boolean checkRegionMatches(final String str, final int strStartIndex, final String search) {
return str.regionMatches(false, strStartIndex, search, 0, search.length());
}
}