use std::borrow::Cow;
use std::path::Path;
use crate::tree::{self, Tree};
use crate::{Error, Result, manifest};
pub const SKILL_FILE: &str = manifest::FILE_NAME;
#[derive(Clone, Debug)]
pub struct Skill {
name: String,
description: String,
dir: String,
digest: String,
tree: Tree,
}
impl Skill {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn description(&self) -> &str {
&self.description
}
#[must_use]
pub fn dir(&self) -> &str {
&self.dir
}
#[must_use]
pub fn digest(&self) -> &str {
&self.digest
}
pub(crate) fn tree(&self) -> &Tree {
&self.tree
}
}
#[derive(Clone, Debug, Default)]
pub struct SkillSet {
skills: Vec<Skill>,
}
impl SkillSet {
#[cfg(feature = "include_dir")]
pub fn from_include_dir(dir: &'static include_dir::Dir<'static>) -> Result<Self> {
fn walk(dir: &'static include_dir::Dir<'static>, out: &mut Vec<tree::File>) {
for file in dir.files() {
out.push(tree::File {
path: file.path().to_string_lossy().replace('\\', "/"),
data: Cow::Borrowed(file.contents()),
executable: false,
});
}
for sub in dir.dirs() {
walk(sub, out);
}
}
let mut files = Vec::new();
walk(dir, &mut files);
Self::from_tree_files(files)
}
pub fn read_dir(root: &Path) -> Result<Self> {
let files = tree::read_dir_raw(root)
.map_err(|e| Error::io(format!("read {}", root.display()), e))?;
Self::from_tree_files(files)
}
pub fn from_files(files: impl IntoIterator<Item = File>) -> Result<Self> {
Self::from_tree_files(files.into_iter().map(|f| f.0).collect())
}
fn from_tree_files(files: Vec<tree::File>) -> Result<Self> {
let mut paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();
paths.sort_unstable();
if let Some([path, _]) = paths.windows(2).find(|w| w[0] == w[1]) {
return Err(Error::Skills(format!("{path} was given twice")));
}
let junk: Vec<String> =
files.iter().filter(|f| tree::is_junk(&f.path)).map(|f| f.path.clone()).collect();
let whole = Tree::from_files(files);
let mut skills = if whole.get(SKILL_FILE).is_some() {
vec![read_skill(&whole, "")?]
} else {
let mut dirs: Vec<&str> = whole
.files()
.iter()
.filter_map(|f| f.path.split_once('/').map(|(d, _)| d))
.collect();
dirs.dedup();
dirs.iter()
.filter(|d| whole.get(&format!("{d}/{SKILL_FILE}")).is_some())
.map(|d| read_skill(&whole, d))
.collect::<Result<Vec<_>>>()?
};
if skills.is_empty() {
return Err(Error::Skills(format!("no {SKILL_FILE} found among the embedded files")));
}
let inside = |p: &String| {
skills
.iter()
.any(|sk| sk.dir.is_empty() || p.starts_with(&format!("{}/", sk.dir)))
};
if let Some(path) = junk.iter().find(|p| inside(p)) {
return Err(Error::Skills(format!(
"{path} was left behind by an operating system and was embedded with a skill. \
Remove the file"
)));
}
skills.sort_by(|a, b| a.name.cmp(&b.name));
check_names(&skills)?;
Ok(Self { skills })
}
#[must_use]
pub fn skills(&self) -> &[Skill] {
&self.skills
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&Skill> {
self.skills.iter().find(|s| s.name == name)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.skills.iter().map(Skill::name)
}
#[must_use]
pub fn len(&self) -> usize {
self.skills.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.skills.is_empty()
}
}
#[derive(Clone, Debug)]
pub struct File(tree::File);
impl File {
#[must_use]
pub fn new(path: impl Into<String>, data: impl Into<Cow<'static, [u8]>>) -> Self {
Self(tree::File { path: path.into(), data: data.into(), executable: false })
}
}
fn read_skill(whole: &Tree, dir: &str) -> Result<Skill> {
let path = if dir.is_empty() { SKILL_FILE.to_owned() } else { format!("{dir}/{SKILL_FILE}") };
let src = whole.get(&path).ok_or_else(|| Error::Skills(format!("{path} is missing")))?;
let fields = manifest::fields(src);
let name = match fields.get("name") {
Some(name) if !name.is_empty() => name.clone(),
_ if dir.is_empty() => {
return Err(Error::Skills(format!(
"the {SKILL_FILE} at the root of the tree has no `name` field, \
and there is no directory to take one from"
)));
}
_ => dir.rsplit('/').next().unwrap_or(dir).to_owned(),
};
check_name(&name).map_err(|e| Error::Skills(format!("{dir}: {e}")))?;
let tree = if dir.is_empty() { whole.clone() } else { whole.subtree(dir) };
Ok(Skill {
digest: tree.digest(),
description: fields.get("description").cloned().unwrap_or_default(),
dir: dir.to_owned(),
name,
tree,
})
}
fn check_name(name: &str) -> Result<(), String> {
if name.is_empty() || name == "." || name == ".." {
return Err(format!("invalid skill name {name:?}"));
}
if name.contains(['/', '\\']) {
return Err(format!("skill name {name:?} must not contain a path separator"));
}
if name.starts_with('.') {
return Err(format!("skill name {name:?} must not start with a dot"));
}
Ok(())
}
fn check_names(skills: &[Skill]) -> Result<()> {
let mut seen: Vec<(String, &Skill)> = Vec::new();
for sk in skills {
let key = sk.name.to_lowercase();
if let Some((_, prev)) = seen.iter().find(|(k, _)| *k == key) {
return Err(Error::Skills(if prev.name == sk.name {
format!("{} and {} both declare the skill name {:?}", prev.dir, sk.dir, sk.name)
} else {
format!(
"{} and {} declare {:?} and {:?}, which differ only in case \
and install over each other on a case insensitive file system",
prev.dir, sk.dir, prev.name, sk.name
)
}));
}
seen.push((key, sk));
}
Ok(())
}