///|
/// A compiled root-relative file matcher. Empty patterns match no files.
/// Matching is pure; selection and security checks remain the caller's job.
struct FileMatcher {
  patterns : Array[Pattern]
}

///|
/// Compile the same bounded custom globs used for includes, without walking files.
pub fn file_matcher(
  patterns : Array[String],
) -> FileMatcher raise SelectionError {
  for pattern in patterns {
    if pattern.contains("\u0000") {
      raise InvalidPattern(
        source="keep-full",
        line=0,
        pattern~,
        detail="NUL is not supported",
      )
    }
  }
  { patterns: custom_patterns(patterns, "keep-full", root_relative=true), }
}

///|
/// Match a normalized root-relative file, including descendants of matched dirs.
pub fn FileMatcher::matches(self : FileMatcher, relative : String) -> Bool {
  if self.patterns.is_empty() ||
    relative.is_empty() ||
    relative.has_prefix("/") ||
    relative.contains("\\") ||
    relative.contains(":") ||
    relative.contains("\u0000") {
    return false
  }
  let parts = relative.split("/").map(part => part.to_owned()).to_array()
  if parts.any(part => part == "" || part == "." || part == "..") {
    return false
  }
  included_parts(self.patterns, parts[:])
}