CredentialScanUnitSource.java
package org.egothor.methodatlas.command;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import org.egothor.methodatlas.api.CredentialScanUnit;
/**
* Selects files to scan for credentials by mask and reads each into a
* {@link CredentialScanUnit}. The default mask is the test-file suffix list; an
* optional glob override widens the scan to arbitrary source.
*
* @since 4.1.0
*/
public final class CredentialScanUnitSource {
private static final Logger LOG = Logger.getLogger(CredentialScanUnitSource.class.getName());
private final List<String> suffixes;
private final PathMatcher globMatcher;
/**
* Creates a source.
*
* @param suffixes test-file suffixes used when no glob override is given; never {@code null}
* @param glob optional glob (e.g. {@code "**}{@code /*.java"}); {@code null} uses {@code suffixes}
*/
public CredentialScanUnitSource(List<String> suffixes, String glob) {
this.suffixes = List.copyOf(suffixes);
this.globMatcher = glob == null ? null
: FileSystems.getDefault().getPathMatcher("glob:" + glob);
}
/**
* Walks {@code roots} and returns one unit per matching, readable file, in
* deterministic sorted order.
*
* @param roots scan roots; never {@code null}
* @return scan units; never {@code null}
*/
public List<CredentialScanUnit> collect(List<Path> roots) {
List<CredentialScanUnit> units = new ArrayList<>();
for (Path root : roots) {
try (Stream<Path> walk = Files.walk(root)) {
walk.filter(Files::isRegularFile)
.filter(this::matches)
.sorted()
.forEach(p -> readInto(p, units));
} catch (IOException e) {
throw new UncheckedIOException("Failed to walk " + root, e);
}
}
return units;
}
private boolean matches(Path p) {
if (globMatcher != null) {
return globMatcher.matches(p);
}
String name = p.getFileName().toString();
return suffixes.stream().anyMatch(name::endsWith);
}
private void readInto(Path p, List<CredentialScanUnit> units) {
try {
String source = Files.readString(p, StandardCharsets.UTF_8);
units.add(new CredentialScanUnit(p.toAbsolutePath(), null, source, languageOf(p)));
} catch (IOException e) {
// Non-fatal: skip unreadable / binary files.
LOG.log(Level.FINE, e, () -> "Skipping unreadable file: " + p);
}
}
/**
* Derives a short language identifier from a file's extension.
*
* @param p the file path; never {@code null}
* @return the lowercase extension (without the dot), or {@code null} when the
* path has no file name or no extension
*/
/* default */ static String languageOf(Path p) {
Path fileName = p.getFileName();
if (fileName == null) {
return null;
}
String name = fileName.toString();
int dot = name.lastIndexOf('.');
return dot >= 0 ? name.substring(dot + 1) : null;
}
}