/** * Copyright (C) 2014 SignalFuse, Inc. */ package com.github.dockerjava.core; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import com.github.dockerjava.core.exception.GoLangFileMatchException; /** * Implementation of golang's file.Match * * Match returns true if name matches the shell file name pattern. The pattern syntax is: * * <pre> * pattern: * { term } * term: * '*' matches any sequence of non-Separator characters * '?' matches any single non-Separator character * '[' [ '^' ] { character-range } ']' * character class (must be non-empty) * c matches character c (c != '*', '?', '\\', '[') * '\\' c matches character c * * character-range: * c matches character c (c != '\\', '-', ']') * '\\' c matches character c * lo '-' hi matches character c for lo <= c <= hi * * Match requires pattern to match all of name, not just a substring. * The only possible returned error is ErrBadPattern, when pattern * is malformed. * * On Windows, escaping is disabled. Instead, '\\' is treated as AuthConfigTest * path separator. * </pre> * * @author tedo * */ public class GoLangFileMatch { private GoLangFileMatch() { } public static final boolean IS_WINDOWS = File.separatorChar == '\\'; private static final String PATTERN_CHARS_TO_ESCAPE = "\\.[]{}()*+-?^$|"; public static boolean match(List<String> patterns, File file) { return !match(patterns, file.getPath()).isEmpty(); } public static boolean match(String pattern, File file) { return match(pattern, file.getPath()); } /** * Returns the matching patterns for the given string */ public static List<String> match(List<String> patterns, String name) { List<String> matches = new ArrayList<String>(); for (String pattern : patterns) { if (match(pattern, name)) { matches.add(pattern); } } return matches; } public static boolean match(String pattern, String name) { return buildPattern(pattern).matcher(name).matches(); } private static Pattern buildPattern(String pattern) { StringBuilder patternStringBuilder = new StringBuilder("^"); while (!pattern.isEmpty()) { pattern = appendChunkPattern(patternStringBuilder, pattern); if (!pattern.isEmpty()) { patternStringBuilder.append(quote(File.separatorChar)); } } patternStringBuilder.append("(").append(quote(File.separatorChar)).append(".*").append(")?"); return Pattern.compile(patternStringBuilder.toString()); } private static String quote(char separatorChar) { if (StringUtils.contains(PATTERN_CHARS_TO_ESCAPE, separatorChar)) { return "\\" + separatorChar; } else { return String.valueOf(separatorChar); } } private static String appendChunkPattern(StringBuilder patternStringBuilder, String pattern) { if (pattern.equals("**") || pattern.startsWith("**" + File.separator)) { patternStringBuilder.append("(") .append("[^").append(quote(File.separatorChar)).append("]*") .append("(") .append(quote(File.separatorChar)).append("[^").append(quote(File.separatorChar)).append("]*") .append(")*").append(")?"); return pattern.substring(pattern.length() == 2 ? 2 : 3); } boolean inRange = false; int rangeFrom = 0; RangeParseState rangeParseState = RangeParseState.CHAR_EXPECTED; boolean isEsc = false; int i; for (i = 0; i < pattern.length(); i++) { char c = pattern.charAt(i); switch (c) { case '/': if (!inRange) { if (!IS_WINDOWS && !isEsc) { // end of chunk return pattern.substring(i + 1); } else { patternStringBuilder.append(quote(c)); } } else { rangeParseState = nextStateAfterChar(rangeParseState); } isEsc = false; break; case '\\': if (!inRange) { if (!IS_WINDOWS) { if (isEsc) { patternStringBuilder.append(quote(c)); isEsc = false; } else { isEsc = true; } } else { // end of chunk return pattern.substring(i + 1); } } else { if (IS_WINDOWS || isEsc) { rangeParseState = nextStateAfterChar(rangeParseState); isEsc = false; } else { isEsc = true; } } break; case '[': if (!isEsc) { if (inRange) { throw new GoLangFileMatchException("[ not expected, closing bracket ] not yet reached"); } rangeFrom = i; rangeParseState = RangeParseState.CHAR_EXPECTED; inRange = true; } else { if (!inRange) { patternStringBuilder.append(c); } else { rangeParseState = nextStateAfterChar(rangeParseState); } } isEsc = false; break; case ']': if (!isEsc) { if (!inRange) { throw new GoLangFileMatchException("] is not expected, [ was not met"); } if (rangeParseState == RangeParseState.CHAR_EXPECTED_AFTER_DASH) { throw new GoLangFileMatchException("Character range not finished"); } patternStringBuilder.append(pattern.substring(rangeFrom, i + 1)); inRange = false; } else { if (!inRange) { patternStringBuilder.append(c); } else { rangeParseState = nextStateAfterChar(rangeParseState); } } isEsc = false; break; case '*': if (!inRange) { if (!isEsc) { patternStringBuilder.append("[^").append(quote(File.separatorChar)).append("]*"); } else { patternStringBuilder.append(quote(c)); } } else { rangeParseState = nextStateAfterChar(rangeParseState); } isEsc = false; break; case '?': if (!inRange) { if (!isEsc) { patternStringBuilder.append("[^").append(quote(File.separatorChar)).append("]"); } else { patternStringBuilder.append(quote(c)); } } else { rangeParseState = nextStateAfterChar(rangeParseState); } isEsc = false; break; case '-': if (!inRange) { patternStringBuilder.append(quote(c)); } else { if (!isEsc) { if (rangeParseState != RangeParseState.CHAR_OR_DASH_EXPECTED) { throw new GoLangFileMatchException("- character not expected"); } rangeParseState = RangeParseState.CHAR_EXPECTED_AFTER_DASH; } else { rangeParseState = nextStateAfterChar(rangeParseState); } } isEsc = false; break; default: if (!inRange) { patternStringBuilder.append(quote(c)); } else { rangeParseState = nextStateAfterChar(rangeParseState); } isEsc = false; } } if (isEsc) { throw new GoLangFileMatchException("Escaped character missing"); } if (inRange) { throw new GoLangFileMatchException("Character range not finished"); } return ""; } private static RangeParseState nextStateAfterChar(RangeParseState currentState) { if (currentState == RangeParseState.CHAR_EXPECTED_AFTER_DASH) { return RangeParseState.CHAR_EXPECTED; } else { return RangeParseState.CHAR_OR_DASH_EXPECTED; } } private enum RangeParseState { CHAR_EXPECTED, CHAR_OR_DASH_EXPECTED, CHAR_EXPECTED_AFTER_DASH } }