use anyhow::Result;
use cargo_metadata::MetadataCommand;
use std::{
cell::RefCell,
collections::HashMap,
fs,
io::ErrorKind,
path::{Path, PathBuf},
};
use tokio::process::Command;
use crate::llm::tools::{
DirectoryEntry, DirectoryEntryKind, DirectoryListing, ResourceType, ToolError,
io_error_to_tool_error,
};
type PackageId = String;
#[derive(Debug, Clone, PartialEq)]
pub struct Package {
pub id: PackageId,
pub name: String,
pub source: Option<PathBuf>,
pub version: String,
}
#[derive(Debug, Clone)]
pub struct PackageResolver {
pub active_pacmans: Vec<String>,
pub packages: RefCell<HashMap<PackageId, Package>>,
pub path_traversal: PathTraversal,
pub refresh_deps: RefCell<bool>,
pub workspace_dir: PathBuf,
}
impl PackageResolver {
pub async fn new(
workspace_dir: PathBuf,
observe_hidden: bool,
observe_ignore: bool,
) -> PackageResolver {
let cargo_success = Command::new("cargo")
.args(["metadata", "--no-deps", "--format-version", "1"])
.current_dir(&workspace_dir)
.output()
.await
.map_or_else(
|err| {
false
},
|val| val.status.success(),
);
if cargo_success {
PackageResolver {
active_pacmans: vec!["cargo".to_string()],
packages: RefCell::new(HashMap::new()),
path_traversal: PathTraversal::new(&workspace_dir, observe_hidden, observe_ignore),
refresh_deps: RefCell::new(true),
workspace_dir,
}
} else {
PackageResolver {
active_pacmans: Vec::new(),
packages: RefCell::new(HashMap::new()),
path_traversal: PathTraversal::new(&workspace_dir, observe_hidden, observe_ignore),
refresh_deps: RefCell::new(true),
workspace_dir,
}
}
}
pub fn package_root(&self, id: Option<&str>) -> Result<Option<String>, ToolError> {
match id {
Some(pckg_id) => {
if pckg_id.is_empty() || pckg_id == "\"\"" || pckg_id == "workspace" {
Ok(None)
} else {
self.package_source(pckg_id).map(Some)
}
}
None => Ok(None),
}
}
pub fn package_source(&self, id: &str) -> Result<String, ToolError> {
self.refresh_dependencies();
let packages = self.packages.borrow();
packages
.get(id)
.ok_or(ToolError::InvalidArgs {
argument: id.to_string(),
reason: format!(
"id '{}' does not denote a package. Search packages with find_package().", id,
),
})
.and_then(|pckg| match &pckg.source {
Some(path) => Ok(path.to_string_lossy().to_string()),
None => Err(ToolError::ResourceNotFound {
resource_type: ResourceType::Path,
identifier: id.to_string(),
}),
})
}
pub fn list_pacmans(&self) -> Vec<String> {
self.active_pacmans.clone()
}
pub fn search_dependency(&self, pattern: &str) -> Vec<String> {
self.refresh_dependencies();
let packages = self.packages.borrow();
packages
.iter()
.filter(|pckg| pckg.1.name.contains(pattern))
.map(|pckg| pckg.0.to_owned())
.collect()
}
fn refresh_dependencies(&self) {
let mut refresh_deps = self.refresh_deps.borrow_mut();
let mut packages = self.packages.borrow_mut();
if *refresh_deps {
*refresh_deps = false;
let metadata = MetadataCommand::new()
.current_dir(&self.workspace_dir)
.exec()
.unwrap();
for package in metadata.packages.iter() {
packages.insert(
format!("{}:{}@{}", "cargo", package.name, package.version),
Package {
id: package.id.repr.clone(),
name: package.name.to_string(),
source: package
.manifest_path
.parent()
.map(|p| p.to_path_buf().into_std_path_buf()),
version: package.version.to_string(),
},
);
}
}
}
}
#[derive(Clone, Debug)]
pub struct PathTraversal {
observe_hidden: bool,
observe_ignore: bool,
package_matcher: RefCell<Option<ignore::IncrementalIgnore>>,
workspace_matcher: RefCell<ignore::IncrementalIgnore>,
}
impl PathTraversal {
pub fn new(root: &Path, observe_hidden: bool, observe_ignore: bool) -> Self {
let workspace_matcher = Self::make_matcher(root, observe_hidden, observe_ignore);
Self {
observe_hidden,
observe_ignore,
package_matcher: RefCell::new(None),
workspace_matcher: RefCell::new(workspace_matcher),
}
}
pub fn absolute(&self, path: &Path, package_root: Option<&Path>) -> Result<PathBuf, ToolError> {
let matcher = self.get_matcher(package_root);
let mut start_path = matcher.root().to_path_buf();
start_path.push(path);
matcher
.normalize(&start_path)
.ok_or(ToolError::PathTraversal {
requested: start_path.to_string_lossy().to_string(),
base: matcher.root().to_string_lossy().to_string(),
})
}
pub fn list_dir(
&self,
path: &Path,
recursive: bool,
package_root: Option<&Path>,
) -> Result<DirectoryListing, ToolError> {
let mut result = DirectoryListing {
entries: Vec::new(),
total_entries: 0,
truncated: false,
};
let path = self.cleaned_path(path);
let mut maybe_matcher = self.package_matcher.borrow_mut();
let matcher = if let Some(pckg_root) = package_root {
if let Some(ref mut pckgm) = *maybe_matcher
&& pckgm.root() == pckg_root
{
pckgm
} else {
*maybe_matcher = Some(Self::make_matcher(
pckg_root,
self.observe_hidden,
self.observe_ignore,
));
maybe_matcher
.as_mut()
.expect("Package matcher should exist right after craetion.")
}
} else {
&mut self.workspace_matcher.borrow_mut()
};
let mut start_path = matcher.root().to_path_buf();
start_path.push(&path);
start_path = matcher
.normalize(&start_path)
.ok_or(ToolError::PathTraversal {
requested: start_path.to_string_lossy().to_string(),
base: matcher.root().to_string_lossy().to_string(),
})?;
if !matcher.root().join(&start_path).is_dir() {
return Result::Err(ToolError::DirectoryEntryNotFound {
path: start_path.to_string_lossy().to_string(),
kind: DirectoryEntryKind::Directory,
suggestion: "Provide a directory to the list_dir tool.".to_string(),
});
}
let mut backlog = vec![start_path];
while let Some(dir) = backlog.pop() {
let absolute_path = matcher.root().join(&dir);
let paths = match fs::read_dir(absolute_path) {
Ok(paths) => paths,
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
continue;
}
Err(err) => return Err(io_error_to_tool_error(err, &path)),
};
for entry in paths.flatten() {
let is_dir = entry.path().is_dir();
let entry = matcher
.normalize(entry.path())
.ok_or(ToolError::PathTraversal {
requested: entry.path().to_string_lossy().to_string(),
base: matcher.root().to_string_lossy().to_string(),
})?;
if matcher.matched(&entry, is_dir).is_ignore() {
continue;
}
if is_dir {
if recursive {
backlog.push(entry.clone());
}
result.entries.push(DirectoryEntry {
path: entry.to_string_lossy().to_string(),
kind: DirectoryEntryKind::Directory,
});
} else {
result.entries.push(DirectoryEntry {
path: entry.to_string_lossy().to_string(),
kind: DirectoryEntryKind::File,
});
}
}
}
result.total_entries = result.entries.len();
Ok(result)
}
fn cleaned_path(&self, path: &Path) -> PathBuf {
if path == "\"\"" || path == "." {
PathBuf::new()
} else {
PathBuf::from(path)
}
}
fn make_matcher(
root: &Path,
observe_hidden: bool,
observe_ignore: bool,
) -> ignore::IncrementalIgnore {
ignore::WalkBuilder::new(root)
.require_git(false)
.parents(true)
.hidden(observe_hidden)
.ignore(observe_ignore)
.git_ignore(observe_ignore)
.git_exclude(observe_ignore)
.git_global(observe_ignore)
.build_matchers()
.first()
.expect("We should get exactly one matcher")
.to_owned()
}
fn get_matcher(&self, package_root: Option<&Path>) -> ignore::IncrementalIgnore {
let mut maybe_matcher = self.package_matcher.borrow_mut();
if let Some(pckg_root) = package_root {
if let Some(pckgm) = (*maybe_matcher).as_ref()
&& pckgm.root() == pckg_root
{
pckgm.clone()
} else {
*maybe_matcher = Some(Self::make_matcher(
pckg_root,
self.observe_hidden,
self.observe_ignore,
));
maybe_matcher
.as_ref()
.expect("Package matcher should exist right after creation.")
.clone()
}
} else {
self.workspace_matcher.borrow().clone()
}
}
}