use std::path::{Component, Path, PathBuf};
use crate::skip::SkillSkip;
pub(super) fn one_line_error(raw: impl std::fmt::Display) -> String {
crate::sanitize_error_token(&raw.to_string())
}
pub(super) fn nfkc_dot_path_components(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in path.components() {
match c {
Component::Normal(s) => {
if let Some(text) = s.to_str() {
let n = crate::parse::normalize_skill_name(text);
let n = n.trim();
if n == "." {
out.push(".");
continue;
}
if n == ".." {
out.push("..");
continue;
}
}
out.push(s);
}
other => out.push(other.as_os_str()),
}
}
out
}
pub(super) fn nfkc_absolute_rewrites_to_fs_root(raw: &str) -> bool {
if raw == "/" || raw == "/.." {
return false;
}
let rewritten = nfkc_dot_path_components(Path::new(raw));
let mut comps = rewritten.components();
match (comps.next(), comps.next(), comps.next()) {
(Some(Component::RootDir), None, _) => true,
(Some(Component::RootDir), Some(Component::ParentDir), None) => true,
(Some(Component::RootDir), Some(Component::Normal(s)), None) if s == ".." => true,
_ => false,
}
}
pub(super) fn str_has_line_separator(s: &str) -> bool {
s.chars()
.any(|ch| matches!(ch, '\n' | '\r' | '\u{2028}' | '\u{2029}'))
}
pub(super) fn host_token_collapses_after_whitespace(raw: &str) -> bool {
let trimmed = raw.trim();
if trimmed.is_empty() {
return false;
}
let n = crate::parse::normalize_skill_name(trimmed);
let n = n.trim();
if n == "." || n == ".." {
return false;
}
if trimmed != raw && Path::new(trimmed).is_absolute() {
return true;
}
if Path::new(raw).components().any(|c| match c {
Component::Normal(s) => s.to_str().is_some_and(|t| {
let inner = t.trim();
if inner.is_empty() {
return true;
}
let n = crate::parse::normalize_skill_name(inner);
let n = n.trim();
if n == "." || n == ".." {
return false;
}
t != inner
}),
_ => false,
}) {
return true;
}
nfkc_absolute_rewrites_to_fs_root(raw)
}
pub(super) fn path_has_line_separator(p: &Path) -> bool {
p.components().any(|c| {
let Some(s) = c.as_os_str().to_str() else {
return false;
};
str_has_line_separator(s)
})
}
#[derive(Clone, Copy)]
pub(super) enum HostPathField {
ExtraPath,
UserDir,
Validate,
}
impl HostPathField {
pub(super) fn collapse_detail(self) -> &'static str {
match self {
Self::ExtraPath => "--path / paths token collapses after whitespace trim",
Self::UserDir => "--user-dir / user_dir token collapses after whitespace trim",
Self::Validate => {
"validate / skills_validate path token collapses after whitespace trim"
}
}
}
pub(super) fn line_sep_detail(self) -> &'static str {
match self {
Self::ExtraPath => "--path / paths component contains a line separator",
Self::UserDir => "--user-dir / user_dir component contains a line separator",
Self::Validate => "validate / skills_validate path component contains a line separator",
}
}
fn missing_path_detail(self, shown: &str) -> String {
match self {
Self::ExtraPath => format!(
"path does not exist: {shown} (pass --path / paths as a SKILL.md file or a package directory that contains SKILL.md)"
),
Self::UserDir => format!(
"path does not exist: {shown} (pass --user-dir / user_dir as a directory of skill packages)"
),
Self::Validate => format!(
"path does not exist: {shown} (pass a SKILL.md file or a package directory that contains SKILL.md)"
),
}
}
fn not_a_directory_detail(self, shown: &str) -> String {
match self {
Self::ExtraPath => format!(
"--path / paths is not a directory: {shown} (pass a SKILL.md file or a package directory that contains SKILL.md)"
),
Self::UserDir => {
format!("--user-dir / user_dir is not a directory: {shown}")
}
Self::Validate => format!("path is not a directory: {shown}"),
}
}
fn unreadable_path_detail(self, shown: &str) -> String {
match self {
Self::ExtraPath => format!("--path / paths is unreadable: {shown}"),
Self::UserDir => format!("--user-dir / user_dir is unreadable: {shown}"),
Self::Validate => format!("path is unreadable: {shown}"),
}
}
fn token_field(self) -> crate::skip::HostTokenField {
match self {
Self::ExtraPath => crate::skip::HostTokenField::ExtraPath,
Self::UserDir => crate::skip::HostTokenField::UserDir,
Self::Validate => crate::skip::HostTokenField::Validate,
}
}
pub(super) fn home_unset_detail(self) -> &'static str {
match self {
Self::ExtraPath => "--path / paths: HOME is unset",
Self::UserDir => "--user-dir / user_dir: HOME is unset",
Self::Validate => "validate / skills_validate: HOME is unset",
}
}
}
pub(super) fn skip_line_separator_root(
root: &Path,
field: HostPathField,
skips: &mut Vec<SkillSkip>,
) {
let skill_md = root.join("SKILL.md");
skips.push(SkillSkip::host_token_refuse(
PathBuf::from(crate::sanitize_error_token(&skill_md.display().to_string())),
field.line_sep_detail().to_owned(),
field.token_field(),
));
}
pub(super) fn skip_tilde_home_unset(raw: &str, field: HostPathField, skips: &mut Vec<SkillSkip>) {
let shown = crate::sanitize_error_token(raw.trim());
skips.push(SkillSkip::host_token_refuse(
PathBuf::from(&shown),
field.home_unset_detail().to_owned(),
field.token_field(),
));
}
pub(super) fn skip_ignore_tilde_home_unset(raw: &str, skips: &mut Vec<SkillSkip>) {
let shown = crate::sanitize_error_token(raw.trim());
skips.push(SkillSkip::host_token_refuse_ignore(
PathBuf::from(&shown),
"--ignore: HOME is unset".to_owned(),
));
}
pub(super) fn skip_whitespace_collapse_token(
raw: &str,
field: HostPathField,
skips: &mut Vec<SkillSkip>,
) {
skips.push(SkillSkip::host_token_refuse(
PathBuf::from(crate::sanitize_error_token(raw)),
field.collapse_detail().to_owned(),
field.token_field(),
));
}
pub(super) fn skip_unresolvable_host_path(
path: &Path,
field: HostPathField,
skips: &mut Vec<SkillSkip>,
) {
let shown = crate::sanitize_error_token(&path.display().to_string());
let detail = match std::fs::metadata(path) {
Ok(meta) if meta.is_dir() => return,
Ok(_) => field.not_a_directory_detail(&shown),
Err(e)
if e.kind() == std::io::ErrorKind::NotFound
|| e.kind() == std::io::ErrorKind::NotADirectory =>
{
field.missing_path_detail(&shown)
}
Err(_) => field.unreadable_path_detail(&shown),
};
let detail = one_line_error(detail);
skips.push(SkillSkip::host_token_refuse(
PathBuf::from(shown),
detail,
field.token_field(),
));
}
pub(super) fn component_is_whitespace_padded_dot(s: &str) -> bool {
let n = crate::parse::normalize_skill_name(s);
let trimmed = n.trim();
if trimmed != "." && trimmed != ".." {
return false;
}
if s == "." || s == ".." {
return false;
}
n != "." && n != ".."
}