blob: 1ea7a0dd3ad574895b64ca284f338bde3f7feec4 [file] [log] [blame]
/*
* 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.
*/
package org.netbeans.modules.docker;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.netbeans.api.annotations.common.CheckForNull;
import org.openide.util.Pair;
/**
*
* @author Petr Hejl
*/
public class IgnorePattern {
private final List<Rule> rules;
private final boolean negative;
private IgnorePattern(List<Rule> rules, boolean negative) {
this.rules = rules;
this.negative = negative;
}
@CheckForNull
public static IgnorePattern compile(String pattern, Character separator, boolean exclusionSupported) {
String trimmed = pattern.trim();
boolean negative = false;
if (exclusionSupported && trimmed.startsWith("!")) {
negative = true;
trimmed = trimmed.substring(1).trim();
}
char sep = separator != null ? separator : File.separatorChar;
String preprocessed = preprocess(trimmed, sep);
// remove the leading / or \ we will match the relative paths
if (preprocessed.startsWith(Character.toString(sep))) {
preprocessed = preprocessed.substring(1);
}
return compilePattern(preprocessed, sep, negative);
}
static String preprocess(String pattern, char separator) {
String sep = Character.toString(separator);
String trimmed = pattern.trim();
String volume = getVolume(trimmed, separator);
String path = trimmed.substring(volume.length());
String ret = path.replaceAll("(" + Pattern.quote(sep) + "){2,}", Matcher.quoteReplacement(sep))
.replaceAll("(" + Pattern.quote(sep) + "\\.)+(" + Pattern.quote(sep) + "|$)", Matcher.quoteReplacement(sep));
if (ret.endsWith(sep) && ret.length() > 1) {
ret = ret.substring(0, ret.length() - sep.length());
}
String[] parts = ret.split(Pattern.quote(sep));
if (parts.length > 1) {
boolean root = false;
StringBuilder removed = new StringBuilder();
int count = 0;
for (int i = parts.length - 1; i >= 0; i--) {
if (parts[i].isEmpty()) {
root = true;
break;
}
if (parts[i].equals("..")) {
count++;
} else {
if (count == 0) {
if (removed.length() > 0) {
removed.insert(0, sep);
}
removed.insert(0, parts[i]);
} else {
count--;
}
}
}
for (int i = 0; i < count; i++) {
if (removed.length() > 0) {
removed.insert(0, sep);
}
removed.insert(0, "..");
}
if (root) {
removed.insert(0, sep);
}
ret = removed.toString();
}
ret = ret.replaceAll("^(" + Pattern.quote(sep) + "\\.\\.)+(" + Pattern.quote(sep) +")?", Matcher.quoteReplacement(sep))
.replaceAll("/", Matcher.quoteReplacement(sep));
if (ret.isEmpty()) {
ret = ".";
}
return volume + ret;
}
static IgnorePattern compilePattern(String pattern, char separator, boolean negative) {
String trimmed = pattern.trim();
List<Rule> ret = new ArrayList<>();
char[] patternChars = trimmed.toCharArray();
List<Character> buffer = new ArrayList<>();
for (int i = 0; i < patternChars.length; i++) {
char c = patternChars[i];
switch (c) {
case '*':
addCharacterListRule(ret, buffer);
if (ret.isEmpty() || !(ret.get(ret.size() - 1) instanceof StarRule)) {
ret.add(new StarRule(separator));
}
break;
case '?':
addCharacterListRule(ret, buffer);
ret.add(new QuestionRule(separator));
break;
case '[':
addCharacterListRule(ret, buffer);
Pair<? extends Rule, Integer> p = createRange(patternChars, i, separator);
ret.add(p.first());
if (p.second() < 0) {
return new IgnorePattern(ret, negative);
}
i = p.second();
break;
case '\\':
if (separator == '\\') {
buffer.add(patternChars[i]);
} else {
if (i < patternChars.length - 1) {
buffer.add(patternChars[++i]);
} else {
addCharacterListRule(ret, buffer);
ret.add(new ErrorRule(trimmed, i));
return new IgnorePattern(ret, negative);
}
}
break;
default:
buffer.add(patternChars[i]);
break;
}
}
addCharacterListRule(ret, buffer);
return new IgnorePattern(ret, negative);
}
private static void addCharacterListRule(List<Rule> rules, List<Character> buffer) {
if (!buffer.isEmpty()) {
rules.add(new CharacterListRule(buffer));
buffer.clear();
}
}
public boolean matches(String input) throws PatternSyntaxException {
return matches(rules, input);
}
public boolean isNegative() {
return negative;
}
boolean isError() {
for (Rule r : rules) {
if (r instanceof ErrorRule) {
return true;
}
}
return false;
}
private static boolean matches(List<Rule> rules, String input) throws PatternSyntaxException {
char[] inputChars = input.toCharArray();
int i = 0;
int listIndex = 0;
for (Iterator<Rule> it = rules.iterator(); it.hasNext();) {
Rule r = it.next();
//try {
if (inputChars.length == 0) {
// star matches even empty string
return rules.size() == 1 && r.matchesEmpty();
}
int[] test = r.consume(inputChars, i);
if (test == null) {
return false;
}
if (test.length == 1) {
i = test[0];
} else if (listIndex == rules.size() - 1
&& test[test.length - 1] >= input.length()) {
// last rule - take the longest one
i = test[test.length - 1];
} else {
for (int j = test.length - 1; j >= 0; j--) {
if (matches(rules.subList(listIndex + 1, rules.size()), input.substring(test[j]))) {
return true;
}
}
return false;
}
// } catch (PatternSyntaxException ex) {
// return false;
// }
listIndex++;
if (i >= inputChars.length) {
if (!it.hasNext()) {
return true;
} else {
return it.next().matchesEmpty();
}
}
}
return i >= inputChars.length;
}
private static Pair<? extends Rule, Integer> createRange(char[] chars, int offset, char separator) {
if (chars[offset] != '[' || offset >= chars.length - 1) {
return Pair.of(new ErrorRule(new String(chars), offset), -1);
//throw new PatternSyntaxException("Malformed range", new String(chars), offset);
}
boolean negated = false;
int start = offset + 1;
char first = chars[offset + 1];
if (first == '^') {
negated = true;
start++;
}
if (start >= chars.length - 1) {
return Pair.of(new ErrorRule(new String(chars), start), -1);
//throw new PatternSyntaxException("Malformed range", new String(chars), start);
}
Character last = null;
LinkedList<Character> singles = new LinkedList<>();
List<Pair<Character, Character>> ranges = new LinkedList<>();
boolean inRange = false;
for (int i = start; i < chars.length; i++) {
char c = chars[i];
switch (c) {
case '\\':
if (separator == '\\') {
char l = chars[i];
if (inRange) {
ranges.add(Pair.of(last, l));
inRange = false;
last = null;
} else {
last = l;
singles.add(l);
}
} else {
if (i < chars.length - 1) {
char l = chars[++i];
// XXX is backslash allowed in range ?
if (inRange) {
ranges.add(Pair.of(last, l));
inRange = false;
last = null;
} else {
last = l;
singles.add(l);
}
} else {
return Pair.of(new ErrorRule(new String(chars), i), -1);
//throw new PatternSyntaxException("Malformed range", new String(chars), i);
}
}
break;
case ']':
if (inRange || i == start) {
return Pair.of(new ErrorRule(new String(chars), i), -1);
//throw new PatternSyntaxException("Malformed range", new String(chars), i);
}
return Pair.of(new RangeRule(negated, ranges, singles), i);
case '-':
if (last == null) {
return Pair.of(new ErrorRule(new String(chars), i), -1);
//throw new PatternSyntaxException("Malformed range", new String(chars), i);
}
singles.removeLast();
inRange = true;
break;
default:
char l = chars[i];
if (inRange) {
ranges.add(Pair.of(last, l));
inRange = false;
last = null;
} else {
last = l;
singles.add(l);
}
break;
}
}
return Pair.of(new ErrorRule(new String(chars), chars.length - 1), -1);
//throw new PatternSyntaxException("Malformed range", new String(chars), chars.length - 1);
}
private static String getVolume(String path, char separator) {
if (separator != '\\') {
return "";
}
if (path.length() < 2) {
return "";
}
char drive = path.charAt(0);
if (path.charAt(1) == ':' && ('a' <= drive && drive <= 'z' || 'A' <= drive && drive <= 'Z')) { // NOI18N
return path.substring(0, 2);
}
// FIXME UNC
return "";
}
private static interface Rule {
int[] consume(char[] chars, int offset);
boolean matchesEmpty();
}
private static class StarRule implements Rule {
private final char separator;
public StarRule(char separator) {
this.separator = separator;
}
@Override
public int[] consume(char[] chars, int offset) {
if (offset >= chars.length) {
throw new IllegalArgumentException();
}
int limit = -1;
for (int i = offset; i < chars.length; i++) {
if (chars[i] == separator) {
limit = i;
break;
}
}
if (limit < 0) {
limit = chars.length;
}
int[] ret = new int[limit - offset + 1];
for (int i = 0; i < ret.length; i++) {
ret[i] = offset + i;
}
return ret;
}
@Override
public boolean matchesEmpty() {
return true;
}
}
private static class QuestionRule implements Rule {
private final char separator;
public QuestionRule(char separator) {
this.separator = separator;
}
@Override
public int[] consume(char[] chars, int offset) {
if (offset >= chars.length) {
throw new IllegalArgumentException();
}
if (chars[offset] == separator) {
return null;
}
return new int[]{offset + 1};
}
@Override
public boolean matchesEmpty() {
return false;
}
}
private static class RangeRule implements Rule {
private final boolean negated;
private final List<Pair<Character, Character>> ranges;
private final List<Character> singles;
public RangeRule(boolean negated, List<Pair<Character, Character>> ranges, List<Character> singles) {
this.negated = negated;
this.ranges = ranges;
this.singles = singles;
}
@Override
public int[] consume(char[] chars, int offset) {
if (offset >= chars.length) {
throw new IllegalArgumentException();
}
boolean ok = check(chars[offset]);
if (negated) {
ok = !ok;
}
if (!ok) {
return null;
}
return new int[]{offset + 1};
}
@Override
public boolean matchesEmpty() {
return false;
}
private boolean check(char c) {
for (Character s : singles) {
if (s == c) {
return true;
}
}
for (Pair<Character, Character> r : ranges) {
if (r.first() <= c && c <= r.second()) {
return true;
}
}
return false;
}
}
private static class CharacterListRule implements Rule {
private final List<Character> array;
public CharacterListRule(List<Character> array) {
this.array = new ArrayList<>(array);
}
@Override
public int[] consume(char[] chars, int offset) throws IllegalStateException {
if (offset >= chars.length) {
throw new IllegalArgumentException();
}
for (int i = 0; i < array.size(); i++) {
if (array.get(i) != chars[offset + i]) {
return null;
}
}
return new int[]{offset + array.size()};
}
@Override
public boolean matchesEmpty() {
return false;
}
}
private static class ErrorRule implements Rule {
private final String regex;
private final int index;
public ErrorRule(String regex, int index) {
this.regex = regex;
this.index = index;
}
@Override
public int[] consume(char[] chars, int offset) {
throw new PatternSyntaxException("Malformed pattern", regex, index);
}
@Override
public boolean matchesEmpty() {
return false;
}
}
}