1use std::borrow::Cow;
2use std::path::Path;
3
4use crate::tree::{self, Tree};
5use crate::{Error, Result, manifest};
6
7pub const SKILL_FILE: &str = manifest::FILE_NAME;
10
11#[derive(Clone, Debug)]
13pub struct Skill {
14 name: String,
15 description: String,
16 dir: String,
17 digest: String,
18 tree: Tree,
19}
20
21impl Skill {
22 #[must_use]
27 pub fn name(&self) -> &str {
28 &self.name
29 }
30
31 #[must_use]
33 pub fn description(&self) -> &str {
34 &self.description
35 }
36
37 #[must_use]
39 pub fn dir(&self) -> &str {
40 &self.dir
41 }
42
43 #[must_use]
46 pub fn digest(&self) -> &str {
47 &self.digest
48 }
49
50 pub(crate) fn tree(&self) -> &Tree {
51 &self.tree
52 }
53}
54
55#[derive(Clone, Debug, Default)]
57pub struct SkillSet {
58 skills: Vec<Skill>,
59}
60
61impl SkillSet {
62 #[cfg(feature = "include_dir")]
82 pub fn from_include_dir(dir: &'static include_dir::Dir<'static>) -> Result<Self> {
83 fn walk(dir: &'static include_dir::Dir<'static>, out: &mut Vec<tree::File>) {
84 for file in dir.files() {
85 out.push(tree::File {
86 path: file.path().to_string_lossy().replace('\\', "/"),
87 data: Cow::Borrowed(file.contents()),
88 executable: false,
89 });
90 }
91 for sub in dir.dirs() {
92 walk(sub, out);
93 }
94 }
95 let mut files = Vec::new();
96 walk(dir, &mut files);
97 Self::from_tree_files(files)
98 }
99
100 pub fn read_dir(root: &Path) -> Result<Self> {
110 let files = tree::read_dir_raw(root)
111 .map_err(|e| Error::io(format!("read {}", root.display()), e))?;
112 Self::from_tree_files(files)
113 }
114
115 pub fn from_files(files: impl IntoIterator<Item = File>) -> Result<Self> {
128 Self::from_tree_files(files.into_iter().map(|f| f.0).collect())
129 }
130
131 fn from_tree_files(files: Vec<tree::File>) -> Result<Self> {
132 let mut paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();
137 paths.sort_unstable();
138 if let Some([path, _]) = paths.windows(2).find(|w| w[0] == w[1]) {
139 return Err(Error::Skills(format!("{path} was given twice")));
140 }
141
142 let junk: Vec<String> =
143 files.iter().filter(|f| tree::is_junk(&f.path)).map(|f| f.path.clone()).collect();
144 let whole = Tree::from_files(files);
145 let mut skills = if whole.get(SKILL_FILE).is_some() {
146 vec![read_skill(&whole, "")?]
147 } else {
148 let mut dirs: Vec<&str> = whole
149 .files()
150 .iter()
151 .filter_map(|f| f.path.split_once('/').map(|(d, _)| d))
152 .collect();
153 dirs.dedup();
154 dirs.iter()
155 .filter(|d| whole.get(&format!("{d}/{SKILL_FILE}")).is_some())
156 .map(|d| read_skill(&whole, d))
157 .collect::<Result<Vec<_>>>()?
158 };
159 if skills.is_empty() {
160 return Err(Error::Skills(format!("no {SKILL_FILE} found among the embedded files")));
161 }
162 let inside = |p: &String| {
171 skills
172 .iter()
173 .any(|sk| sk.dir.is_empty() || p.starts_with(&format!("{}/", sk.dir)))
175 };
176 if let Some(path) = junk.iter().find(|p| inside(p)) {
177 return Err(Error::Skills(format!(
178 "{path} was left behind by an operating system and was embedded with a skill. \
179 Remove the file"
180 )));
181 }
182 skills.sort_by(|a, b| a.name.cmp(&b.name));
183 check_names(&skills)?;
184 Ok(Self { skills })
185 }
186
187 #[must_use]
189 pub fn skills(&self) -> &[Skill] {
190 &self.skills
191 }
192
193 #[must_use]
195 pub fn get(&self, name: &str) -> Option<&Skill> {
196 self.skills.iter().find(|s| s.name == name)
197 }
198
199 pub fn names(&self) -> impl Iterator<Item = &str> {
201 self.skills.iter().map(Skill::name)
202 }
203
204 #[must_use]
206 pub fn len(&self) -> usize {
207 self.skills.len()
208 }
209
210 #[must_use]
212 pub fn is_empty(&self) -> bool {
213 self.skills.is_empty()
214 }
215}
216
217#[derive(Clone, Debug)]
219pub struct File(tree::File);
220
221impl File {
222 #[must_use]
225 pub fn new(path: impl Into<String>, data: impl Into<Cow<'static, [u8]>>) -> Self {
226 Self(tree::File { path: path.into(), data: data.into(), executable: false })
227 }
228}
229
230fn read_skill(whole: &Tree, dir: &str) -> Result<Skill> {
231 let path = if dir.is_empty() { SKILL_FILE.to_owned() } else { format!("{dir}/{SKILL_FILE}") };
232 let src = whole.get(&path).ok_or_else(|| Error::Skills(format!("{path} is missing")))?;
233 let fields = manifest::fields(src);
234 let name = match fields.get("name") {
235 Some(name) if !name.is_empty() => name.clone(),
236 _ if dir.is_empty() => {
240 return Err(Error::Skills(format!(
241 "the {SKILL_FILE} at the root of the tree has no `name` field, \
242 and there is no directory to take one from"
243 )));
244 }
245 _ => dir.rsplit('/').next().unwrap_or(dir).to_owned(),
246 };
247 check_name(&name).map_err(|e| Error::Skills(format!("{dir}: {e}")))?;
248
249 let tree = if dir.is_empty() { whole.clone() } else { whole.subtree(dir) };
250 Ok(Skill {
251 digest: tree.digest(),
252 description: fields.get("description").cloned().unwrap_or_default(),
253 dir: dir.to_owned(),
254 name,
255 tree,
256 })
257}
258
259fn check_name(name: &str) -> Result<(), String> {
262 if name.is_empty() || name == "." || name == ".." {
263 return Err(format!("invalid skill name {name:?}"));
264 }
265 if name.contains(['/', '\\']) {
266 return Err(format!("skill name {name:?} must not contain a path separator"));
267 }
268 if name.starts_with('.') {
269 return Err(format!("skill name {name:?} must not start with a dot"));
270 }
271 Ok(())
272}
273
274fn check_names(skills: &[Skill]) -> Result<()> {
279 let mut seen: Vec<(String, &Skill)> = Vec::new();
280 for sk in skills {
281 let key = sk.name.to_lowercase();
282 if let Some((_, prev)) = seen.iter().find(|(k, _)| *k == key) {
283 return Err(Error::Skills(if prev.name == sk.name {
284 format!("{} and {} both declare the skill name {:?}", prev.dir, sk.dir, sk.name)
285 } else {
286 format!(
287 "{} and {} declare {:?} and {:?}, which differ only in case \
288 and install over each other on a case insensitive file system",
289 prev.dir, sk.dir, prev.name, sk.name
290 )
291 }));
292 }
293 seen.push((key, sk));
294 }
295 Ok(())
296}