use crate::manifest::Manifest;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::path::Path;
const MAX_GLOB_PATTERNS: usize = 20;
const MAX_CONSECUTIVE_WILDCARDS: usize = 2;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DiscoveredService {
pub name: String,
pub path: String,
pub markers_found: Vec<String>,
}
pub fn discover_services(root: &Path, manifest: &Manifest) -> Vec<DiscoveredService> {
discover_services_with_ignore(root, manifest, &[])
}
pub fn discover_services_with_ignore(
root: &Path,
manifest: &Manifest,
extra_ignore: &[String],
) -> Vec<DiscoveredService> {
discover_services_with_opts(root, manifest, extra_ignore, 1)
}
pub fn discover_services_with_opts(
root: &Path,
manifest: &Manifest,
extra_ignore: &[String],
depth: u32,
) -> Vec<DiscoveredService> {
let base_patterns = manifest.effective_discovery_paths();
let markers = &manifest.discovery.markers;
if base_patterns.len() > MAX_GLOB_PATTERNS {
eprintln!(
"warning: too many discovery patterns ({}, max {}). Some patterns will be ignored.",
base_patterns.len(),
MAX_GLOB_PATTERNS
);
}
let effective_markers: Vec<String> = if markers.is_empty() {
default_markers()
} else {
markers.clone()
};
let ignore_patterns: Vec<glob::Pattern> = manifest
.discovery
.ignore
.iter()
.chain(extra_ignore.iter())
.filter_map(|p| {
if has_dangerous_pattern(p) {
eprintln!(
"warning: ignore pattern '{}' may be expensive and was skipped",
p
);
None
} else {
glob::Pattern::new(p).ok()
}
})
.collect();
let mut patterns: Vec<String> = Vec::new();
for (idx, base) in base_patterns.iter().enumerate() {
if idx >= MAX_GLOB_PATTERNS {
break;
}
if has_dangerous_pattern(base) {
eprintln!(
"warning: discovery pattern '{}' may be expensive and was skipped",
base
);
continue;
}
let mut current = base.clone();
patterns.push(current.clone());
for _ in 1..depth.max(1) {
current.push_str("/*");
patterns.push(current.clone());
}
}
let results: Vec<DiscoveredService> = patterns
.par_iter()
.flat_map(|pattern| {
let mut pattern_discovered = Vec::new();
let full_pattern = root.join(pattern);
let pattern_str = full_pattern.to_string_lossy();
let entries = match glob::glob(&pattern_str) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
for entry in entries.flatten() {
let metadata = match entry.metadata() {
Ok(m) => m,
Err(_) => continue,
};
if metadata.is_symlink() {
eprintln!(
"warning: skipping symlink '{}' during discovery (potential security risk)",
entry.display()
);
continue;
}
if !metadata.is_dir() {
continue;
}
let rel_path = entry
.strip_prefix(root)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| entry.to_string_lossy().to_string());
if ignore_patterns.iter().any(|p| p.matches(&rel_path)) {
continue;
}
let found_markers = markers_in_dir(&entry, &effective_markers);
if found_markers.is_empty() {
continue;
}
let name = entry
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
pattern_discovered.push(DiscoveredService {
name,
path: rel_path,
markers_found: found_markers,
});
}
pattern_discovered
})
.collect();
let mut discovered: Vec<DiscoveredService> = Vec::new();
let mut seen_paths: std::collections::HashSet<String> = std::collections::HashSet::new();
for service in results {
if seen_paths.insert(service.path.clone()) {
discovered.push(service);
}
}
discovered
}
pub fn dir_has_markers(root: &Path, rel_path: &str, markers: &[String]) -> bool {
let full = root.join(rel_path);
if !full.is_dir() {
return false;
}
!markers_in_dir(&full, markers).is_empty()
}
fn markers_in_dir(dir: &Path, markers: &[String]) -> Vec<String> {
markers
.iter()
.filter(|m| dir.join(m.as_str()).exists())
.cloned()
.collect()
}
fn default_markers() -> Vec<String> {
[
"Cargo.toml",
"Dockerfile",
"go.mod",
"package.json",
"pyproject.toml",
"requirements.txt",
]
.iter()
.map(|s| s.to_string())
.collect()
}
fn has_dangerous_pattern(pattern: &str) -> bool {
let mut consecutive_wildcards = 0;
for c in pattern.chars() {
if c == '*' {
consecutive_wildcards += 1;
if consecutive_wildcards > MAX_CONSECUTIVE_WILDCARDS {
return true; }
} else {
consecutive_wildcards = 0;
}
}
if pattern == "**" || pattern == "*" || pattern == "**/*" {
return false; }
if pattern.starts_with("**/") && !pattern.contains("/**/") {
return false;
}
false
}