use std::path::{Path, PathBuf};
use super::ContentError;
pub const DEV_CLASS_SOURCES: &[(&str, &str)] = &[
("agents", "content/agents"),
("skills", "content/skills"),
("instructions", "content/instructions"),
(
"instructions/output-styles",
"content/instructions/output-styles",
),
(
"instructions/sm_instructions",
"content/instructions/sm_instructions",
),
(
"instructions/harness_understanding",
"content/instructions/harness_understanding",
),
];
const GIT_MARKER: &str = ".git";
const WORKSPACE_MANIFEST: &str = "Cargo.toml";
pub fn find_dev_checkout(start: &Path) -> Option<PathBuf> {
find_dev_checkout_as(start, current_euid())
}
pub(super) fn find_dev_checkout_as(start: &Path, euid: Option<u32>) -> Option<PathBuf> {
let root = start
.ancestors()
.find(|dir| dir.join(GIT_MARKER).exists())?;
check_checkout(root, euid).ok().map(|()| root.to_path_buf())
}
pub(super) fn require_checkout(root: &Path) -> Result<(), ContentError> {
check_checkout(root, current_euid())
}
pub(super) type OwnerFn = dyn Fn(&Path, &std::fs::Metadata) -> Option<u32>;
pub(super) fn check_checkout(root: &Path, euid: Option<u32>) -> Result<(), ContentError> {
check_checkout_with(root, euid, &file_owner)
}
pub(super) fn check_checkout_with(
root: &Path,
euid: Option<u32>,
owner: &OwnerFn,
) -> Result<(), ContentError> {
if let Some(missing) = first_missing_class(root) {
return Err(ContentError::NotACheckout {
root: root.to_path_buf(),
missing,
});
}
let untrusted = |reason: String| ContentError::UntrustedCheckout {
root: root.to_path_buf(),
reason,
};
if !root.join(GIT_MARKER).exists() {
return Err(untrusted(format!("it has no {GIT_MARKER}")));
}
check_owner(root, euid, owner).map_err(untrusted)?;
if !is_workspace_manifest(&root.join(WORKSPACE_MANIFEST)) {
return Err(untrusted(format!(
"its {WORKSPACE_MANIFEST} has no [workspace] table"
)));
}
Ok(())
}
fn is_workspace_manifest(path: &Path) -> bool {
std::fs::read_to_string(path)
.ok()
.and_then(|text| text.parse::<toml::Table>().ok())
.is_some_and(|table| table.get("workspace").is_some_and(toml::Value::is_table))
}
fn check_owner(root: &Path, euid: Option<u32>, owner: &OwnerFn) -> Result<(), String> {
let Some(euid) = euid else { return Ok(()) };
let unreadable = |path: &Path, e: std::io::Error| {
format!("the owner of {} cannot be read: {e}", path.display())
};
let root_meta = std::fs::metadata(root).map_err(|e| unreadable(root, e))?;
if world_writable(&root_meta) {
return Err("it is writable by every user".to_owned());
}
let git = root.join(GIT_MARKER);
let manifest = root.join(WORKSPACE_MANIFEST);
let git_meta = std::fs::symlink_metadata(&git).map_err(|e| unreadable(&git, e))?;
let manifest_meta =
std::fs::symlink_metadata(&manifest).map_err(|e| unreadable(&manifest, e))?;
for (label, path, meta) in [
("it", root, &root_meta),
(GIT_MARKER, git.as_path(), &git_meta),
(WORKSPACE_MANIFEST, manifest.as_path(), &manifest_meta),
] {
let uid = owner(path, meta).ok_or_else(|| format!("the owner of {label} is unknown"))?;
if uid != euid {
return Err(format!(
"{label} is owned by uid {uid}, not the current user (uid {euid})"
));
}
}
Ok(())
}
#[cfg(unix)]
pub(super) fn file_owner(_path: &Path, meta: &std::fs::Metadata) -> Option<u32> {
use std::os::unix::fs::MetadataExt;
Some(meta.uid())
}
#[cfg(not(unix))]
pub(super) fn file_owner(_path: &Path, _meta: &std::fs::Metadata) -> Option<u32> {
None
}
#[cfg(unix)]
fn world_writable(meta: &std::fs::Metadata) -> bool {
use std::os::unix::fs::PermissionsExt;
meta.permissions().mode() & 0o002 != 0
}
#[cfg(not(unix))]
fn world_writable(_meta: &std::fs::Metadata) -> bool {
false
}
#[cfg(unix)]
pub(super) fn current_euid() -> Option<u32> {
Some(unsafe { libc::geteuid() })
}
#[cfg(not(unix))]
pub(super) fn current_euid() -> Option<u32> {
None
}
fn first_missing_class(root: &Path) -> Option<PathBuf> {
DEV_CLASS_SOURCES
.iter()
.map(|(_, rel)| root.join(rel))
.find(|dir| !dir.is_dir())
}
pub(super) fn read_regular(root: &Path, key: &str) -> Result<Option<Vec<u8>>, ContentError> {
let Some((dest, source)) = owner(key) else {
return Ok(None);
};
let Some(rest) = key.strip_prefix(dest).and_then(|r| r.strip_prefix('/')) else {
return Ok(None);
};
let mut path = root.join(source);
let mut parts = rest.split('/').peekable();
while let Some(part) = parts.next() {
if part.starts_with('.') {
return Ok(None);
}
path.push(part);
let kind = match std::fs::symlink_metadata(&path) {
Ok(meta) => meta.file_type(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => return Err(ContentError::Io { path, source }),
};
let expected = if parts.peek().is_some() {
kind.is_dir()
} else {
kind.is_file()
};
if !expected {
return Ok(None);
}
}
match std::fs::read(&path) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(source) => Err(ContentError::Io { path, source }),
}
}
fn owner(key: &str) -> Option<(&'static str, &'static str)> {
DEV_CLASS_SOURCES
.iter()
.copied()
.filter(|(dest, _)| is_under(key, dest))
.max_by_key(|(dest, _)| dest.len())
}
fn is_under(key: &str, dir: &str) -> bool {
key.strip_prefix(dir)
.is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
}
pub(super) fn list(root: &Path, prefix: &str) -> Result<Vec<String>, ContentError> {
let mut out = Vec::new();
for (dest, source) in DEV_CLASS_SOURCES
.iter()
.filter(|(d, _)| is_under(d, prefix))
{
let mut keys = Vec::new();
walk(&root.join(source), dest, &mut keys)?;
out.extend(
keys.into_iter()
.filter(|k| owner(k).is_some_and(|(d, _)| d == *dest)),
);
}
out.sort();
Ok(out)
}
fn walk(dir: &Path, prefix: &str, out: &mut Vec<String>) -> Result<(), ContentError> {
let io_err = |source| ContentError::Io {
path: dir.to_path_buf(),
source,
};
for entry in std::fs::read_dir(dir).map_err(io_err)? {
let entry = entry.map_err(io_err)?;
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
if name.starts_with('.') {
continue;
}
let kind = entry.file_type().map_err(io_err)?;
let key = format!("{prefix}/{name}");
if kind.is_dir() {
walk(&entry.path(), &key, out)?;
} else if kind.is_file() {
out.push(key);
}
}
Ok(())
}