DirectoryScanner.java
/*
* Copyright 2024 Emmanuel Bourg
*
* 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 net.jsign;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Scans a directory recursively and returns the files matching a pattern.
*
* @since 7.0
*/
class DirectoryScanner {
/**
* Scans the current directory for files matching the specified pattern.
*
* @param glob the glob pattern ({@code foo/**}{@code /*bar/*.exe})
*/
public List<Path> scan(String glob) throws IOException {
// normalize the pattern
glob = glob.replace('\\', '/').replace("/+", "/");
// adjust the base directory
String basedir = findBaseDirectory(glob);
// strip the base directory from the pattern
glob = glob.substring(basedir.length());
Pattern pattern = Pattern.compile(globToRegExp(glob));
int maxDepth = maxPatternDepth(glob);
// let's scan the files
List<Path> matches = new ArrayList<>();
Files.walkFileTree(new File(basedir).toPath(), new FileVisitor<Path>() {
private int depth = -1;
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
String name = dir.getFileName().toString();
if (depth + 1 > maxDepth || ".svn".equals(name) || ".git".equals(name)) {
return FileVisitResult.SKIP_SUBTREE;
} else {
depth++;
return FileVisitResult.CONTINUE;
}
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
String filename = file.toString();
filename = filename.replaceAll("\\\\", "/");
if (filename.startsWith("./")) {
filename = filename.substring(2);
}
if (filename.startsWith(basedir)) {
filename = filename.substring(basedir.length());
}
if (pattern.matcher(filename).matches()) {
matches.add(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
depth--;
return FileVisitResult.CONTINUE;
}
});
return matches;
}
/**
* Converts a glob pattern into a regular expression.
*
* @param glob the glob pattern to convert ({@code foo/**}{@code /bar/*.exe})
*/
String globToRegExp(String glob) {
String delimiters = "/\\";
StringTokenizer tokenizer = new StringTokenizer(glob, delimiters, true);
boolean ignoreNextSeparator = false;
StringBuilder pattern = new StringBuilder();
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (token.length() == 1 && delimiters.contains(token)) {
if (!ignoreNextSeparator) {
pattern.append("/");
}
} else if ("**".equals(token)){
pattern.append("(?:|.*/)");
ignoreNextSeparator = true;
} else if (token.contains("*")) {
ignoreNextSeparator = false;
pattern.append("\\Q").append(token.replaceAll("\\*", "\\\\E[^/]*\\\\Q")).append("\\E");
} else {
ignoreNextSeparator = false;
pattern.append("\\Q").append(token).append("\\E");
}
}
return pattern.toString().replaceAll("/+", "/");
}
/**
* Finds the base directory of the specified pattern.
*/
String findBaseDirectory(String pattern) {
Pattern regexp = Pattern.compile("([^*]*/).*");
Matcher matcher = regexp.matcher(pattern);
if (matcher.matches()) {
return matcher.group(1);
} else {
return "";
}
}
/**
* Returns the maximum depth of the pattern (stripped from its base directory).
*/
int maxPatternDepth(String pattern) {
if (pattern.contains("**")) {
return 50;
}
int depth = 0;
for (int i = 0; i < pattern.length(); i++) {
if (pattern.charAt(i) == '/') {
depth++;
}
}
return depth;
}
}