use std::path::{Path, PathBuf};
use crate::paths;
use crate::types::Task;
pub(crate) fn owned_output_path(task: &Task) -> Option<PathBuf> {
let declared = task.output_path.as_deref()?;
let path = Path::new(declared);
if path.is_absolute() {
return path.is_file().then(|| path.to_path_buf());
}
for base in task_output_bases(task) {
if let Some(candidate) = owned_file_under_base(&base, path) {
return Some(candidate);
}
}
None
}
fn owned_file_under_base(base: &Path, relative: &Path) -> Option<PathBuf> {
let candidate = base.join(relative);
let canon_base = base.canonicalize().ok()?;
let canon_file = candidate.canonicalize().ok()?;
if !canon_file.starts_with(&canon_base) || !canon_file.is_file() {
return None;
}
Some(canon_file)
}
fn task_output_bases(task: &Task) -> Vec<PathBuf> {
let mut bases = Vec::with_capacity(3);
push_unique_base(&mut bases, task.effective_dir.as_deref());
push_unique_base(&mut bases, task.worktree_path.as_deref());
push_unique_base(&mut bases, Some(paths::task_dir(task.id.as_str())));
bases
}
fn push_unique_base(bases: &mut Vec<PathBuf>, raw: Option<impl AsRef<Path>>) {
let Some(raw) = raw else { return };
let path = raw.as_ref().to_path_buf();
if path.as_os_str().is_empty() || bases.iter().any(|base| base == &path) {
return;
}
bases.push(path);
}
pub(crate) fn missing_owned_output_declared(task: &Task) -> Option<&str> {
let declared = task.output_path.as_deref()?;
owned_output_path(task).is_none().then_some(declared)
}
pub(crate) fn missing_owned_output_absence(task: &Task) -> Option<String> {
missing_owned_output_declared(task).map(|declared| {
format!("No task-owned output file for this task (declared: {declared}).")
})
}
pub(super) fn missing_owned_output_notice(task: &Task) -> Option<String> {
missing_owned_output_absence(task)
.map(|absence| format!("{absence} Falling back to this task's log.\n"))
}