use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::Deserialize as _;
use yapi_types::rpc::SourceInfo;
use yapi_types::settings::{DefaultProjectTrust, FilteredPackage};
use super::fetch::placed_name;
use super::lock::fresh;
use super::manifest::{Entry, MANIFEST_ENV, Manifest, Section, from_env};
use super::{Mode, Project};
use crate::extensions::{EventPolicy, Grants};
use crate::packages::npm::{Npm, escaped};
use crate::packages::resolve::{
PackageInput, ProjectOnly, ResolvedPaths, ResourceType, package_resources,
};
use crate::settings::{Scope, SettingsManager};
use crate::tools::path::home_dir;
use crate::trust::{TrustStore, resolve_trusted};
pub fn project_manifest(project_root: &Path) -> Option<PathBuf> {
let name = from_env().ok()?;
Some(project_root.join(name.as_deref().unwrap_or("chest.toml")))
}
pub fn entry_label(source: &str, scope: &str, project_root: &Path) -> Option<String> {
let name = source.strip_prefix("chest:")?;
let manifest = match scope {
"user" => "~/.chest/chest.toml".to_owned(),
_ => {
let manifest = project_manifest(project_root)?;
manifest.file_name()?.to_string_lossy().into_owned()
}
};
Some(format!("extensions.{name} in {manifest}"))
}
fn consumes(manifest: &Manifest) -> bool {
[Section::Skills, Section::Agents, Section::Extensions]
.into_iter()
.any(|section| !manifest.section(section).is_empty())
|| !manifest.patch.is_empty()
}
pub fn needs_trust(project_root: &Path) -> bool {
let Some(Ok(text)) = project_manifest(project_root).map(std::fs::read_to_string) else {
return false;
};
Manifest::parse(&text, Scope::Project).map_or(true, |manifest| consumes(&manifest))
}
#[derive(Clone, Debug, PartialEq)]
pub struct Policy {
pub grants: Grants,
pub events: EventPolicy,
pub root: Option<PathBuf>,
}
impl Policy {
pub fn unrestricted() -> Policy {
Policy {
grants: Grants::default(),
events: EventPolicy::shared(),
root: None,
}
}
pub fn closed(root: &Path) -> Policy {
Policy {
grants: Grants::none(),
events: EventPolicy::isolated(),
root: Some(root.to_path_buf()),
}
}
fn intersection(&self, other: &Policy) -> Policy {
let canonical = |root: &PathBuf| root.canonicalize().unwrap_or_else(|_| root.clone());
let roots = (
self.root.as_ref().map(canonical),
other.root.as_ref().map(canonical),
);
let root = match roots {
(Some(left), Some(right)) if left.starts_with(&right) => Some(left),
(Some(left), Some(right)) if right.starts_with(&left) => Some(right),
_ => None,
};
Policy {
grants: self.grants.intersection(other.grants),
events: self.events.intersection(&other.events),
root,
}
}
}
pub type Policies = HashMap<(String, String), Policy>;
#[derive(Clone, Debug, Default)]
pub struct FilePolicies {
pub files: HashMap<PathBuf, Policy>,
pub closed: Vec<PathBuf>,
}
impl FilePolicies {
pub fn of(&self, file: &Path) -> Policy {
if self.closes(file) {
return Policy::closed(file);
}
self.files
.get(file)
.cloned()
.unwrap_or_else(Policy::unrestricted)
}
pub fn manages(&self, file: &Path) -> bool {
self.files.contains_key(file) || self.closes(file)
}
pub fn closes(&self, file: &Path) -> bool {
self.closed.iter().any(|root| file.starts_with(root))
}
pub fn also_loaded(&self, file: &Path, path: &str, source: &str) -> Option<(String, String)> {
if !self.manages(file) {
return None;
}
let message = match self.closes(file) {
true => format!(
"{path} is also loaded from {source}, but its chest entry does not load, so it runs with no capabilities"
),
false => format!("{path} is also loaded from {source}; chest's grants apply"),
};
Some((escaped(path), escaped(&message)))
}
}
pub fn file_policies(
resolved: &ResolvedPaths,
installed: &Installed,
project_root: &Path,
problems: &mut Vec<(String, String)>,
) -> FilePolicies {
let files = resolved.of(ResourceType::Extensions).iter();
let files = files.map(|file| &file.info).chain(&resolved.shadowed);
let mut found = FilePolicies {
files: HashMap::new(),
closed: installed
.closed
.iter()
.map(|closed| closed.root.clone())
.collect(),
};
let mut givers: Vec<(PathBuf, &SourceInfo, Vec<String>, [bool; 2])> = Vec::new();
for info in files.filter(|info| info.source.starts_with("chest:")) {
let key = (info.scope.clone(), info.source.clone());
let file = Path::new(&info.path);
let file = file.canonicalize().unwrap_or_else(|_| file.into());
let policy = installed.policies.get(&key).cloned().unwrap_or_else(|| {
let message = format!(
"{}: no chest entry grants this file, so it runs with no capabilities",
info.path
);
problems.push((escaped(&info.source), escaped(&message)));
Policy::closed(&file)
});
let label = entry_label(&info.source, &info.scope, project_root);
let label = label.unwrap_or_else(|| info.source.clone());
let policy = match found.files.get(&file) {
Some(other) => {
if let Some((.., labels, [grants, events])) =
givers.iter_mut().find(|giver| giver.0 == file)
{
if !labels.contains(&label) {
labels.push(label);
}
*grants |= other.grants != policy.grants;
*events |= other.events != policy.events;
}
other.intersection(&policy)
}
None => {
givers.push((file.clone(), info, vec![label], [false; 2]));
policy
}
};
found.files.insert(file, policy);
}
let only = |narrowed: [bool; 2], policy: &Policy, count: usize| {
let all = if count == 2 { "both" } else { "all" };
match narrowed {
[false, false] => None,
[true, false] => Some(format!("grants {all} give: {}", policy.grants)),
[false, true] => Some(format!("event channels {all} give")),
[true, true] => Some(format!(
"event channels and the grants {all} give: {}",
policy.grants
)),
}
};
for (file, info, labels, narrowed) in givers {
let (path, source) = (&info.path, &info.source);
let Some(policy) = found.files.get(&file) else {
continue;
};
let closer = installed
.closed
.iter()
.find(|closed| file.starts_with(&closed.root));
if let Some(Closed {
entry: closer,
remove,
install,
..
}) = closer
{
let fix = match install {
Some((command, once)) => {
let [grants, events] = narrowed;
let narrowed = [
grants || policy.grants != once.grants,
events || policy.events != once.events,
];
let after = policy.intersection(once);
let gives = only(narrowed, &after, labels.len() + 1);
let gives = gives.map(|gives| format!(", which gives it only the {gives}"));
format!(
"run `{command}`{}, or `{remove}`",
gives.unwrap_or_default()
)
}
None => format!("run `{remove}`"),
};
let message = format!(
"{path} is in {}, but {closer} holds it too and does not load, so it runs with no capabilities: {fix}",
labels.join(" and ")
);
problems.push((escaped(source), escaped(&message)));
continue;
}
let Some(narrowed) = only(narrowed, policy, labels.len()) else {
continue;
};
let message = format!(
"{path} is in {}, so it gets only the {narrowed}",
labels.join(" and ")
);
problems.push((escaped(source), escaped(&message)));
}
found
}
#[derive(Clone, Debug, Default)]
pub struct Installed {
pub packages: Vec<PackageInput>,
pub skills: Vec<PackageInput>,
pub policies: Policies,
pub closed: Vec<Closed>,
pub identities: Vec<(String, String, String, bool)>,
}
#[derive(Clone, Debug)]
pub struct Closed {
pub root: PathBuf,
pub entry: String,
pub remove: String,
pub install: Option<(String, Policy)>,
}
impl Installed {
fn extend(&mut self, other: Installed) {
self.packages.extend(other.packages);
self.skills.extend(other.skills);
self.policies.extend(other.policies);
self.closed.extend(other.closed);
self.identities.extend(other.identities);
}
}
pub fn reads_global(settings: &SettingsManager) -> bool {
!crate::config::project_only()
|| crate::packages::resolve::ProjectOnly::from_settings(settings.document(Scope::Global))
.needs_chest()
}
#[derive(Clone, Debug, PartialEq)]
enum Install {
Chest {
name: String,
path: String,
global: bool,
},
Settings { scope: Scope, source: String },
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LeftOut(Vec<(Install, Install)>);
impl LeftOut {
pub fn new(
installed: &Installed,
settings: &SettingsManager,
keep: Option<&ProjectOnly>,
agent_dir: &Path,
) -> LeftOut {
let rank = |source: &str, global: bool| match keep {
Some(keep) if global && !keep.keeps_package(source, agent_dir) => {
keep.needs_package(source, agent_dir).then_some(1)
}
_ => Some(0),
};
let chest = |global: bool| {
let identities = installed.identities.iter();
identities
.filter(move |(_, _, _, other)| *other == global)
.filter_map(move |(id, name, path, global)| {
let rank = rank(&format!("chest:{name}"), *global)?;
let (name, path, global) = (name.clone(), path.clone(), *global);
Some((rank, id.clone(), Install::Chest { name, path, global }))
})
};
let from_settings = |scope: Scope| {
let packages = crate::packages::settings_packages(settings, scope);
let sources = packages
.iter()
.filter_map(crate::packages::entry_source)
.filter_map(|source| {
let rank = rank(source, scope == Scope::Global)?;
let id = crate::packages::source::parse(source).identity(Path::new(""));
let source = source.to_owned();
Some((rank, id, Install::Settings { scope, source }))
});
sources.collect::<Vec<_>>()
};
let mut copies: Vec<(u8, String, Install)> = chest(false)
.chain(from_settings(Scope::Project))
.chain(chest(true))
.chain(from_settings(Scope::Global))
.collect();
let pair = |a: &Install, b: &Install| {
!matches!((a, b), (Install::Settings { .. }, Install::Settings { .. }))
};
if let Some(keep) = keep {
let home = home_dir();
let input = |copy: &Install| match copy {
Install::Chest { name, .. } => {
let source = format!("chest:{name}");
let mut inputs = installed.packages.iter();
let found =
inputs.find(|input| input.scope == "user" && input.source == source);
found.cloned()
}
Install::Settings { source, .. } => {
let global = crate::packages::settings_packages(settings, Scope::Global);
let inputs = crate::packages::resolve::package_inputs(
&[],
&global,
|_| agent_dir.to_path_buf(),
|source, _| crate::packages::install_location(source, agent_dir),
);
inputs.into_iter().find(|input| input.source == *source)
}
};
let counts: Vec<bool> = copies
.iter()
.enumerate()
.map(|(index, (rank, id, copy))| {
let mut others = copies.iter().enumerate();
*rank == 0
|| !others.any(|(other_index, (_, other, install))| {
other_index != index && same_package(id, other) && pair(copy, install)
})
|| input(copy)
.is_some_and(|input| keep.keeps_part(&input, agent_dir, &home))
})
.collect();
let copies_counts = copies.into_iter().zip(counts);
copies = copies_counts
.filter_map(|(copy, counts)| counts.then_some(copy))
.collect();
}
copies.sort_by_key(|(rank, ..)| *rank);
let mut left_out = Vec::new();
let mut loaded: Vec<(&str, &Install)> = Vec::new();
for (_, id, copy) in &copies {
let kept = loaded.iter().find(|(other, _)| same_package(other, id));
match kept {
Some((_, kept)) if pair(kept, copy) => {
left_out.push((copy.clone(), (*kept).clone()));
}
_ => loaded.push((id, copy)),
}
}
LeftOut(left_out)
}
pub fn chest(&self, input: &PackageInput) -> bool {
self.0.iter().any(|(copy, _)| match copy {
Install::Chest { name, global, .. } => {
input.source == format!("chest:{name}") && (input.scope == "user") == *global
}
Install::Settings { .. } => false,
})
}
pub fn settings(&self, scope: Scope, source: &str) -> bool {
let copy = Install::Settings {
scope,
source: source.to_owned(),
};
self.0.iter().any(|(left_out, _)| *left_out == copy)
}
pub fn problems(&self, approve: bool) -> Vec<(String, String)> {
let scope = |global: bool| {
if global {
Scope::Global
} else {
Scope::Project
}
};
let home = home_dir();
let home = home.canonicalize().unwrap_or(home);
let place = |copy: &Install| match copy {
Install::Chest { name, path, global } => {
let label = super::manifest_label(Path::new(path), scope(*global), &home);
format!("extensions.{name} in {label}")
}
Install::Settings { scope, source } => match scope {
Scope::Project => format!("{source} in the project's settings"),
Scope::Global => format!("{source} in the global settings"),
},
};
let local = match approve {
true => "yapi remove -l --approve",
false => "yapi remove -l",
};
let problems = self.0.iter().map(|(copy, kept)| {
let remove = match copy {
Install::Chest { name, path, global } => {
let command = super::command(Path::new(path), scope(*global));
format!("{command} remove {name}")
}
Install::Settings { scope, source } => match scope {
Scope::Project => format!("{local} {source}"),
Scope::Global => format!("yapi remove {source}"),
},
};
let message = format!(
"{} is left out, because {} installs the same package: remove it with `{remove}`",
place(copy),
place(kept),
);
let path = match (copy, kept) {
(Install::Chest { path, .. }, _) | (_, Install::Chest { path, .. }) => path.clone(),
_ => String::new(),
};
(escaped(&path), escaped(&message))
});
problems.collect()
}
}
pub fn duplicates(
installed: &Installed,
settings: &SettingsManager,
keep: Option<&ProjectOnly>,
agent_dir: &Path,
) -> Vec<(String, String)> {
let default = settings
.document(Scope::Global)
.get("defaultProjectTrust")
.and_then(|value| DefaultProjectTrust::deserialize(value).ok());
let store = TrustStore::new(agent_dir);
let trusted = resolve_trusted(settings.project_dir(), &store, None, default);
LeftOut::new(installed, settings, keep, agent_dir).problems(!trusted.unwrap_or(false))
}
#[derive(Clone, Debug, Default)]
pub struct Loaded {
pub installed: Installed,
pub problems: Vec<(String, String)>,
}
impl Loaded {
fn warn(&mut self, (path, message): (String, String)) {
self.problems.push((escaped(&path), escaped(&message)));
}
}
pub fn package_identity(entry: &Entry) -> Option<String> {
let package = match (&entry.npm, &entry.git) {
(Some(npm), _) => format!("npm:{npm}"),
(None, Some(git)) => {
crate::packages::source::parse(&format!("git:{git}")).identity(Path::new(""))
}
(None, None) => return None,
};
let path = entry.path.iter().flat_map(|path| path.split('/'));
let path: Vec<&str> = path
.filter(|segment| !matches!(*segment, "" | "."))
.collect();
Some(match path.is_empty() {
true => package,
false => format!("{package}#{}", path.join("/")),
})
}
pub fn same_package(a: &str, b: &str) -> bool {
let folder_of = |whole: &str, folder: &str| {
folder
.strip_prefix(whole)
.is_some_and(|rest| rest.starts_with('#'))
};
a == b || folder_of(a, b) || folder_of(b, a)
}
pub fn installed_inputs(
project: &Project,
project_root: &Path,
problems: &mut Vec<String>,
) -> Installed {
let scope = match project.scope {
Scope::Project => "project",
Scope::Global => "user",
};
let mut installed = Installed::default();
let lock = project.lock.clone().unwrap_or_default();
let record = project.record();
let current = |section: Section, name: &str, entry: &Entry| {
lock.section(section)
.iter()
.find(|locked| locked.name == name)
.filter(|locked| fresh(entry, locked))
.filter(|locked| project.present(section, locked))
.filter(|locked| project.holds_locked(section, entry, locked, &record))
.cloned()
};
for (name, entry) in &project.manifest.extensions {
let patched = project.manifest.patch.contains_key(name);
let roots = project.extension_roots(name);
let label = format!("extensions.{name} in {}", project.label());
if let Some((root, module_root)) = &roots
&& project.too_wide(module_root, &[project_root])
{
problems.push(format!(
"extensions.{name}: {} holds more than the extension, such as the project, the home or the agent folder, so it does not load",
module_root.display()
));
let local = crate::packages::source::is_local(&format!("chest:{name}"));
let files = package_resources(root, None, local).extensions;
let files = files.iter().map(|file| Path::new(&file.info.path));
let remove = format!("{} remove {name}", project.command());
let closed = files.filter_map(|file| file.canonicalize().ok());
let closed = closed.map(|root| Closed {
root,
entry: label.clone(),
remove: remove.clone(),
install: None,
});
installed.closed.extend(closed);
continue;
}
let policy = |root: PathBuf| Policy {
grants: project.manifest.grants(entry),
events: entry.events.clone().unwrap_or_default(),
root: Some(root),
};
if !patched && current(Section::Extensions, name, entry).is_none() {
if let Some(root) = roots.and_then(|(_, root)| root.canonicalize().ok()) {
let install = Some((project.install_command(), policy(root.clone())));
installed.closed.push(Closed {
root,
entry: label,
remove: format!("{} remove {name}", project.command()),
install,
});
}
continue;
}
let Some((root, module_root)) = roots else {
continue;
};
if !root.exists() {
if patched || entry.git.is_none() && entry.npm.is_none() {
problems.push(format!(
"extensions.{name}: {} does not exist",
root.display()
));
}
continue;
}
let source = format!("chest:{name}");
let filter = [&entry.skills, &entry.prompts, &entry.themes]
.iter()
.any(|list| list.is_some())
.then(|| FilteredPackage {
source: source.clone(),
autoload: None,
extensions: None,
skills: entry.skills.clone(),
prompts: entry.prompts.clone(),
themes: entry.themes.clone(),
});
let key = (scope.to_owned(), source.clone());
installed.policies.insert(key, policy(module_root));
if let Some(identity) = package_identity(entry) {
let path = project.path.display().to_string();
let global = project.scope == Scope::Global;
installed
.identities
.push((identity, name.clone(), path, global));
}
installed.packages.push(PackageInput {
source,
scope: scope.to_owned(),
root,
filter,
});
}
let Some(home) = project.homes(Section::Skills).into_iter().next() else {
return installed;
};
for (name, entry) in &project.manifest.skills {
let patch = project.manifest.patch.get(name);
let roots: Vec<PathBuf> = match (
patch.and_then(|patch| patch.path.as_deref()),
current(Section::Skills, name, entry),
) {
(Some(path), _) => {
let patch = project.resolve(path);
if !patch.exists() {
problems.push(format!("skills.{name}: {} does not exist", patch.display()));
continue;
}
let package = entry.path.is_none() && (entry.git.is_some() || entry.npm.is_some());
let items = if package {
match super::fetch::items(&patch, Section::Skills, None, name) {
Ok((items, _)) => items,
Err(err) => {
problems.push(format!("skills.{name}: {err}"));
continue;
}
}
} else {
vec![(placed_name(Section::Skills, name), patch)]
};
items
.into_iter()
.map(|(item, from)| {
let placed = home.join(item);
if placed.exists() { placed } else { from }
})
.collect()
}
(None, Some(locked)) => project
.locked_items(Section::Skills, &locked)
.into_iter()
.map(|(item, _)| home.join(item))
.collect(),
(None, None) => continue,
};
for root in roots {
installed.skills.push(PackageInput {
source: format!("chest:{name}"),
scope: scope.to_owned(),
root,
filter: None,
});
}
}
installed
}
fn manifests(
project_root: &Path,
trusted: bool,
global: bool,
) -> Vec<Result<Project, (String, String)>> {
let mut found = Vec::new();
if trusted && let Err(problem) = from_env() {
let problem = format!("{problem}, so no project manifest loads");
found.push(Err((MANIFEST_ENV.to_owned(), problem)));
}
let project = trusted.then(|| project_manifest(project_root)).flatten();
let project = project.map(|path| (path, Scope::Project));
let global = global.then(|| (home_dir().join(".chest/chest.toml"), Scope::Global));
for (path, scope) in [project, global].into_iter().flatten() {
if !path.exists() {
continue;
}
match Project::open(&path, scope) {
Ok(project) if consumes(&project.manifest) => found.push(Ok(project)),
Ok(_) => {}
Err(err) => found.push(Err((path.display().to_string(), err.to_string()))),
}
}
found
}
fn add(loaded: &mut Loaded, project: &Project, project_root: &Path) {
let mut problems = Vec::new();
loaded
.installed
.extend(installed_inputs(project, project_root, &mut problems));
let path = project.path.display().to_string();
for problem in problems {
loaded.warn((path.clone(), format!("{}: {problem}", project.label())));
}
}
pub fn installed(project_root: &Path, trusted: bool, global: bool) -> Loaded {
let mut loaded = Loaded::default();
for project in manifests(project_root, trusted, global) {
match project {
Ok(project) => add(&mut loaded, &project, project_root),
Err(problem) => loaded.warn(problem),
}
}
loaded
}
pub async fn load(project_root: &Path, trusted: bool, offline: bool, global: bool) -> Loaded {
let mut loaded = Loaded::default();
let mut npm = None;
for project in manifests(project_root, trusted, global) {
let mut project = match project {
Ok(project) => project,
Err(problem) => {
loaded.warn(problem);
continue;
}
};
let path = project.path.display().to_string();
let npm = npm.get_or_insert_with(|| Npm::new(crate::packages::npm::config()));
match project
.sync(Mode::Startup { offline }, npm, &mut |_| {})
.await
{
Ok(report) => {
for message in report.warnings.into_iter().chain(report.notices) {
loaded.warn((path.clone(), message));
}
}
Err(err) => loaded.warn((path.clone(), err.to_string())),
}
add(&mut loaded, &project, project_root);
}
loaded
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packages::resolve::Resolved;
use yapi_types::rpc::SourceInfo;
#[test]
fn startup_load_send() {
fn send<T: Send>(_: T) {}
send(load(Path::new("."), false, true, true));
}
#[test]
fn chest_files_fail_closed() {
let file = |path: &str, source: &str, scope: &str| SourceInfo {
path: path.into(),
source: source.into(),
scope: scope.into(),
origin: "package".into(),
base_dir: None,
};
let enabled = |info| Resolved {
info,
enabled: true,
};
let spoof = "/p/\x1b[2J.js";
let resolved = ResolvedPaths {
extensions: vec![
enabled(file("/p/a.js", "chest:a", "project")),
enabled(file("/p/b.js", "npm:b", "project")),
enabled(file(spoof, "chest:a", "project")),
],
..ResolvedPaths::default()
};
let mut problems = Vec::new();
let policies = file_policies(
&resolved,
&Installed::default(),
Path::new("/p"),
&mut problems,
);
let (file_a, file_spoof) = (PathBuf::from("/p/a.js"), PathBuf::from(spoof));
let files = HashMap::from([
(file_a.clone(), Policy::closed(&file_a)),
(file_spoof.clone(), Policy::closed(&file_spoof)),
]);
assert_eq!(policies.files, files);
assert_eq!(problems.len(), 2, "{problems:?}");
let warned = policies.also_loaded(&file_spoof, spoof, "cli");
let warned = warned
.into_iter()
.flat_map(|(path, message)| [path, message]);
let texts: Vec<String> = problems
.iter()
.flat_map(|(source, message)| [source.clone(), message.clone()])
.chain(warned)
.collect();
assert!(
texts.len() == 6 && texts.iter().all(|text| !text.contains('\x1b')),
"{texts:?}"
);
let rooted = |root: &str| Policy {
root: Some(root.into()),
..Policy::unrestricted()
};
let key = |scope: &str, name: &str| (scope.to_owned(), format!("chest:{name}"));
let filesystem = Grants {
filesystem: true,
..Grants::none()
};
let network = Grants {
network: true,
..Grants::none()
};
let narrow = |grants, events| Policy {
grants,
events,
..rooted("/p/sub")
};
let installed = Installed {
policies: HashMap::from([
(key("project", "s"), rooted("/p")),
(key("user", "s"), narrow(filesystem, EventPolicy::shared())),
(
key("project", "e"),
narrow(filesystem, EventPolicy::shared()),
),
(
key("user", "e"),
narrow(filesystem, EventPolicy::isolated()),
),
(key("project", "g"), rooted("/p")),
(
key("user", "g"),
narrow(filesystem, EventPolicy::isolated()),
),
]),
closed: [("/q", None), ("/p/sub/c.js", Some(network))]
.map(|(root, grants)| Closed {
root: root.into(),
entry: "extensions.x in chest.toml".into(),
remove: "chest remove x".into(),
install: grants.map(|grants| {
let policy = narrow(grants, EventPolicy::shared());
("chest install".to_owned(), policy)
}),
})
.into(),
..Installed::default()
};
let resolved = ResolvedPaths {
extensions: vec![
enabled(file("/p/sub/a.js", "chest:s", "project")),
enabled(file("/p/sub/b.js", "chest:s", "user")),
enabled(file("/p/sub/c.js", "chest:s", "project")),
enabled(file("/p/sub/e.js", "chest:e", "project")),
enabled(file("/p/sub/g.js", "chest:g", "project")),
],
shadowed: vec![
file("/p/sub/a.js", "chest:s", "user"),
file("/p/sub/b.js", "chest:s", "project"),
file("/p/sub/c.js", "chest:s", "user"),
file("/p/sub/e.js", "chest:e", "user"),
file("/p/sub/g.js", "chest:g", "user"),
],
..ResolvedPaths::default()
};
problems.clear();
let policies = file_policies(&resolved, &installed, Path::new("/p"), &mut problems);
let root = |path: &str| policies.of(Path::new(path)).root;
for file in ["/p/sub/a.js", "/p/sub/b.js"] {
assert_eq!(root(file), Some("/p/sub".into()), "{file}");
}
let (project, user) = (
"extensions.s in chest.toml",
"extensions.s in ~/.chest/chest.toml",
);
let shared = |file: &str, first: &str, second: &str| {
let message = format!(
"{file} is in {first} and {second}, so it gets only the grants both give: fs"
);
("chest:s".to_owned(), message)
};
let closed = format!(
"/p/sub/c.js is in {project} and {user}, but extensions.x in chest.toml holds it too and does not load, so it runs with no capabilities: run `chest install`, which gives it only the grants all give: none, or `chest remove x`"
);
let narrowed = |name: &str, what: &str| {
let message = format!(
"/p/sub/{name}.js is in extensions.{name} in chest.toml and extensions.{name} in ~/.chest/chest.toml, so it gets only the {what}"
);
(format!("chest:{name}"), message)
};
let both = [
shared("/p/sub/a.js", project, user),
shared("/p/sub/b.js", user, project),
("chest:s".to_owned(), closed),
narrowed("e", "event channels both give"),
narrowed("g", "event channels and the grants both give: fs"),
];
assert_eq!(problems, both);
assert_eq!(root("/q/a.js"), Some("/q/a.js".into()));
assert_eq!(root("/p/sub/c.js"), Some("/p/sub/c.js".into()));
}
}