use std::path::{Component, Path, PathBuf};
use polyc_agent::delegate::{ShareInCeiling, ShareInError};
use super::search::glob_to_regex;
use super::workspace::{WORKER_SUBDIR_PREFIX, resolve};
struct Selected {
source: PathBuf,
relative: String,
bytes: u64,
}
pub fn seed(
parent_root: &Path,
worker_root: &Path,
requested: &[String],
ceiling: &ShareInCeiling,
) -> Result<Vec<String>, ShareInError> {
if requested.is_empty() {
return Ok(Vec::new());
}
if !ceiling.admits_anything() {
return Err(ShareInError::OutsideCeiling {
path: requested[0].clone(),
});
}
let patterns = compile(ceiling);
let mut selected = Vec::new();
for entry in requested {
collect(parent_root, entry, ceiling, &mut selected)?;
}
if selected.len() > ceiling.max_files {
return Err(ShareInError::TooManyFiles {
found: selected.len(),
limit: ceiling.max_files,
});
}
let total: u64 = selected.iter().map(|f| f.bytes).sum();
if total > ceiling.max_bytes {
return Err(ShareInError::TooManyBytes {
found: total,
limit: ceiling.max_bytes,
});
}
for file in &selected {
if !patterns.iter().any(|re| re.is_match(&file.relative)) {
return Err(ShareInError::OutsideCeiling {
path: file.relative.clone(),
});
}
}
let mut seeded = Vec::with_capacity(selected.len());
for file in selected {
let destination =
resolve(worker_root, &file.relative).map_err(|reason| ShareInError::Escapes {
path: file.relative.clone(),
reason,
})?;
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent).map_err(|err| ShareInError::Io {
path: file.relative.clone(),
reason: err.to_string(),
})?;
}
std::fs::copy(&file.source, &destination).map_err(|err| ShareInError::Io {
path: file.relative.clone(),
reason: err.to_string(),
})?;
seeded.push(file.relative);
}
Ok(seeded)
}
fn compile(ceiling: &ShareInCeiling) -> Vec<regex::Regex> {
ceiling
.allow
.iter()
.filter_map(|pattern| regex::Regex::new(&glob_to_regex(pattern)).ok())
.collect()
}
fn collect(
parent_root: &Path,
entry: &str,
ceiling: &ShareInCeiling,
out: &mut Vec<Selected>,
) -> Result<(), ShareInError> {
let source = resolve(parent_root, entry).map_err(|reason| ShareInError::Escapes {
path: entry.to_owned(),
reason,
})?;
if names_worker_subtree(parent_root, &source) {
return Err(ShareInError::WorkerSubtree {
path: entry.to_owned(),
});
}
let meta = std::fs::symlink_metadata(&source).map_err(|_| ShareInError::NotFound {
path: entry.to_owned(),
})?;
if meta.is_symlink() {
return Err(ShareInError::Escapes {
path: entry.to_owned(),
reason: "symbolic links are never seeded".to_owned(),
});
}
if meta.is_dir() {
walk(parent_root, &source, ceiling, out)
} else {
out.push(Selected {
relative: relative_to(parent_root, &source),
source,
bytes: meta.len(),
});
Ok(())
}
}
fn names_worker_subtree(parent_root: &Path, source: &Path) -> bool {
source
.strip_prefix(parent_root)
.ok()
.and_then(|rel| rel.components().next())
.is_some_and(|first| {
matches!(first, Component::Normal(part)
if part.to_string_lossy().starts_with(WORKER_SUBDIR_PREFIX))
})
}
fn walk(
parent_root: &Path,
dir: &Path,
ceiling: &ShareInCeiling,
out: &mut Vec<Selected>,
) -> Result<(), ShareInError> {
let entries = std::fs::read_dir(dir).map_err(|err| ShareInError::Io {
path: relative_to(parent_root, dir),
reason: err.to_string(),
})?;
for entry in entries.flatten() {
let path = entry.path();
let Ok(meta) = std::fs::symlink_metadata(&path) else {
continue;
};
if meta.is_symlink() {
continue;
}
if meta.is_dir() {
let is_worker_tree = path
.file_name()
.is_some_and(|name| name.to_string_lossy().starts_with(WORKER_SUBDIR_PREFIX));
if is_worker_tree {
continue;
}
walk(parent_root, &path, ceiling, out)?;
} else {
out.push(Selected {
relative: relative_to(parent_root, &path),
source: path,
bytes: meta.len(),
});
}
if out.len() > ceiling.max_files {
return Err(ShareInError::TooManyFiles {
found: out.len(),
limit: ceiling.max_files,
});
}
}
Ok(())
}
fn relative_to(root: &Path, path: &Path) -> String {
path.strip_prefix(root)
.unwrap_or(path)
.components()
.filter_map(|c| match c {
Component::Normal(part) => Some(part.to_string_lossy().into_owned()),
_ => None,
})
.collect::<Vec<_>>()
.join("/")
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
fn ceiling(allow: &[&str], max_files: usize, max_bytes: u64) -> ShareInCeiling {
ShareInCeiling {
allow: allow.iter().map(|s| (*s).to_owned()).collect(),
max_files,
max_bytes,
}
}
fn write(root: &Path, rel: &str, body: &str) {
let path = root.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
fn roots() -> (PathBuf, PathBuf) {
let parent = super::super::tmp_dir("share-in").join("workspace");
let worker = parent.join(".worker-call-a-0000");
std::fs::create_dir_all(&worker).unwrap();
(parent, worker)
}
#[test]
fn seeds_a_named_file_at_the_same_relative_path() {
let (parent, worker) = roots();
write(&parent, "src/parser.rs", "fn parse() {}");
let seeded = seed(
&parent,
&worker,
&["src/parser.rs".to_owned()],
&ceiling(&["src/**"], 10, 1024),
)
.expect("a file inside the ceiling seeds");
assert_eq!(seeded, vec!["src/parser.rs".to_owned()]);
assert_eq!(
std::fs::read_to_string(worker.join("src/parser.rs")).unwrap(),
"fn parse() {}",
);
}
#[test]
fn refuses_a_file_outside_the_ceiling() {
let (parent, worker) = roots();
write(&parent, "secrets/token", "s3cret");
let err = seed(
&parent,
&worker,
&["secrets/token".to_owned()],
&ceiling(&["src/**"], 10, 1024),
)
.expect_err("a file the ceiling does not admit must be refused");
assert!(
matches!(err, ShareInError::OutsideCeiling { .. }),
"{err:?}"
);
assert!(
!worker.join("secrets/token").exists(),
"a refused request must copy nothing"
);
}
#[test]
fn refuses_traversal_out_of_the_parent_workspace() {
let (parent, worker) = roots();
let err = seed(
&parent,
&worker,
&["../outside.txt".to_owned()],
&ceiling(&["**"], 10, 1024),
)
.expect_err("`..` must never leave the parent workspace");
assert!(matches!(err, ShareInError::Escapes { .. }), "{err:?}");
}
#[test]
fn refuses_reaching_into_a_sibling_workers_subtree() {
let (parent, worker) = roots();
write(&parent, ".worker-call-b-1111/notes.md", "sibling's work");
let err = seed(
&parent,
&worker,
&[".worker-call-b-1111/notes.md".to_owned()],
&ceiling(&["**"], 10, 1024),
)
.expect_err("a sibling worker's subtree is never seedable");
assert!(matches!(err, ShareInError::WorkerSubtree { .. }), "{err:?}");
}
#[test]
fn refuses_a_dot_prefixed_path_into_a_worker_subtree() {
let (parent, worker) = roots();
write(&parent, ".worker-call-b-1111/notes.md", "sibling's work");
let err = seed(
&parent,
&worker,
&["./.worker-call-b-1111/notes.md".to_owned()],
&ceiling(&["**"], 10, 1024),
)
.expect_err("`./` must not smuggle a request into a worker subtree");
assert!(matches!(err, ShareInError::WorkerSubtree { .. }), "{err:?}");
}
#[test]
fn refuses_when_over_the_file_bound() {
let (parent, worker) = roots();
for i in 0..5 {
write(&parent, &format!("src/f{i}.rs"), "x");
}
let err = seed(
&parent,
&worker,
&["src".to_owned()],
&ceiling(&["src/**"], 3, 1024),
)
.expect_err("five files must not seed under a three-file ceiling");
assert!(matches!(err, ShareInError::TooManyFiles { .. }), "{err:?}");
}
#[test]
fn refuses_when_over_the_byte_bound() {
let (parent, worker) = roots();
write(&parent, "src/big.rs", &"x".repeat(200));
let err = seed(
&parent,
&worker,
&["src/big.rs".to_owned()],
&ceiling(&["src/**"], 10, 64),
)
.expect_err("a file over the byte ceiling must be refused");
assert!(matches!(err, ShareInError::TooManyBytes { .. }), "{err:?}");
assert!(
!worker.join("src/big.rs").exists(),
"a refused request must copy nothing"
);
}
#[test]
fn seeds_a_directory_recursively() {
let (parent, worker) = roots();
write(&parent, "src/a.rs", "a");
write(&parent, "src/nested/b.rs", "b");
let mut seeded = seed(
&parent,
&worker,
&["src".to_owned()],
&ceiling(&["src/**"], 10, 1024),
)
.expect("a directory seeds every file under it");
seeded.sort();
assert_eq!(
seeded,
vec!["src/a.rs".to_owned(), "src/nested/b.rs".to_owned()]
);
assert_eq!(
std::fs::read_to_string(worker.join("src/nested/b.rs")).unwrap(),
"b"
);
}
#[test]
fn a_closed_ceiling_seeds_nothing() {
let (parent, worker) = roots();
write(&parent, "src/parser.rs", "fn parse() {}");
let err = seed(
&parent,
&worker,
&["src/parser.rs".to_owned()],
&ShareInCeiling::default(),
)
.expect_err("an unconfigured ceiling admits nothing");
assert!(
matches!(err, ShareInError::OutsideCeiling { .. }),
"{err:?}"
);
}
#[test]
fn an_empty_request_seeds_nothing_and_succeeds() {
let (parent, worker) = roots();
write(&parent, "src/parser.rs", "fn parse() {}");
let seeded = seed(&parent, &worker, &[], &ceiling(&["src/**"], 10, 1024))
.expect("no request is not an error");
assert!(seeded.is_empty());
assert!(!worker.join("src/parser.rs").exists());
}
#[test]
fn refuses_a_symlink_named_directly() {
let (parent, worker) = roots();
write(&parent, "src/real.rs", "real");
#[cfg(unix)]
std::os::unix::fs::symlink("/etc/hostname", parent.join("src/link.rs")).unwrap();
#[cfg(unix)]
{
let err = seed(
&parent,
&worker,
&["src/link.rs".to_owned()],
&ceiling(&["src/**"], 10, 1024),
)
.expect_err("a symlink could read outside the workspace");
assert!(matches!(err, ShareInError::Escapes { .. }), "{err:?}");
}
}
#[test]
fn refuses_a_path_that_names_nothing() {
let (parent, worker) = roots();
let err = seed(
&parent,
&worker,
&["src/missing.rs".to_owned()],
&ceiling(&["src/**"], 10, 1024),
)
.expect_err("a path naming nothing is an error, not an empty seed");
assert!(matches!(err, ShareInError::NotFound { .. }), "{err:?}");
}
}