use std::sync::Arc;
use globset::{Glob, GlobSet, GlobSetBuilder};
use thiserror::Error;
use typed_builder::TypedBuilder;
pub const MAX_GLOB_PATTERN_BYTES: usize = 256;
pub const MAX_DISCOVERY_THREADS: u32 = 8;
#[derive(Clone, Debug, TypedBuilder)]
#[non_exhaustive]
pub struct DiscoveryOptions {
#[builder(default = false)]
pub follow_symlinks: bool,
#[builder(default = 16)]
pub max_depth: u32,
#[builder(default = 8 * 1024 * 1024)]
pub max_file_size_bytes: u64,
#[builder(default = 200_000)]
pub max_total_files: u64,
#[builder(default = default_exclude_globset())]
pub exclude_globs: Arc<GlobSet>,
#[builder(default = default_module_globset())]
pub module_globs: Arc<GlobSet>,
#[builder(default = 0)]
pub threads: u32,
}
impl DiscoveryOptions {
#[must_use]
pub fn defaults() -> Self {
Self::builder().build()
}
#[must_use]
pub const fn effective_threads(&self) -> u32 {
if self.threads > MAX_DISCOVERY_THREADS {
MAX_DISCOVERY_THREADS
} else {
self.threads
}
}
}
impl Default for DiscoveryOptions {
fn default() -> Self {
Self::defaults()
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum GlobConfigError {
#[error("glob pattern too long ({observed} > {limit}): `{pattern}`")]
TooLong {
pattern: String,
observed: usize,
limit: usize,
},
#[error("invalid glob pattern `{pattern}`: {source}")]
Invalid {
pattern: String,
#[source]
source: globset::Error,
},
}
pub fn compile_glob_set<I, S>(patterns: I) -> Result<GlobSet, GlobConfigError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut builder = GlobSetBuilder::new();
for pattern in patterns {
let pattern_str = pattern.as_ref();
if pattern_str.len() > MAX_GLOB_PATTERN_BYTES {
return Err(GlobConfigError::TooLong {
pattern: pattern_str.to_owned(),
observed: pattern_str.len(),
limit: MAX_GLOB_PATTERN_BYTES,
});
}
let glob = Glob::new(pattern_str).map_err(|source| GlobConfigError::Invalid {
pattern: pattern_str.to_owned(),
source,
})?;
builder.add(glob);
}
builder.build().map_err(|source| GlobConfigError::Invalid {
pattern: String::new(),
source,
})
}
fn default_exclude_globset() -> Arc<GlobSet> {
let patterns = [
"**/.git",
"**/.git/**",
"**/.terraform",
"**/.terraform/**",
"**/.terragrunt-cache",
"**/.terragrunt-cache/**",
];
let set = compile_glob_set(patterns).unwrap_or_else(|_| GlobSet::empty());
Arc::new(set)
}
fn default_module_globset() -> Arc<GlobSet> {
let patterns = [
"modules/*",
"modules/**",
"modules-tf12/*",
"modules-tf12/**",
"**/modules/*",
"**/modules-tf12/*",
];
let set = compile_glob_set(patterns).unwrap_or_else(|_| GlobSet::empty());
Arc::new(set)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn test_should_build_defaults() {
let opts = DiscoveryOptions::defaults();
assert!(!opts.follow_symlinks);
assert_eq!(opts.max_depth, 16);
assert_eq!(opts.max_file_size_bytes, 8 * 1024 * 1024);
assert_eq!(opts.max_total_files, 200_000);
assert!(opts.exclude_globs.is_match(Path::new(".git/HEAD")));
assert!(
opts.exclude_globs
.is_match(Path::new("services/order-service/.terraform/lockfile"))
);
}
#[test]
fn test_should_match_module_globs_by_default() {
let opts = DiscoveryOptions::defaults();
assert!(opts.module_globs.is_match(Path::new("modules/iam-role")));
assert!(
opts.module_globs
.is_match(Path::new("terraform/modules/vpc"))
);
}
#[test]
fn test_should_compile_user_glob_set() {
let set = compile_glob_set(["foo/**", "bar/*"]).unwrap();
assert!(set.is_match(Path::new("foo/x/y")));
assert!(set.is_match(Path::new("bar/x")));
assert!(!set.is_match(Path::new("baz")));
}
#[test]
fn test_should_reject_overlong_pattern() {
let pattern = "a".repeat(MAX_GLOB_PATTERN_BYTES + 1);
let err = compile_glob_set([pattern]).unwrap_err();
assert!(matches!(err, GlobConfigError::TooLong { .. }));
}
#[test]
fn test_should_reject_invalid_glob_pattern() {
let err = compile_glob_set(["[unterminated"]).unwrap_err();
assert!(matches!(err, GlobConfigError::Invalid { .. }));
}
#[test]
fn test_builder_applies_overrides() {
let opts: DiscoveryOptions = DiscoveryOptions::builder()
.max_depth(4_u32)
.follow_symlinks(true)
.threads(2_u32)
.build();
assert_eq!(opts.max_depth, 4);
assert!(opts.follow_symlinks);
assert_eq!(opts.threads, 2);
}
}