use crate::value::Value;
use anyhow::Result;
fn is_sep(c: char) -> bool {
c == '/' || c == '\\'
}
fn split_root(path: &str) -> (String, &str) {
let bytes = path.as_bytes();
if bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& is_sep(bytes[2] as char)
{
return (format!("{}:/", bytes[0] as char), &path[3..]);
}
if path.starts_with(is_sep) {
return ("/".to_string(), path);
}
(String::new(), path)
}
fn components(rest: &str) -> Vec<&str> {
rest.split(is_sep)
.filter(|part| !part.is_empty() && *part != ".")
.collect()
}
fn name_of(path: &str) -> &str {
components(split_root(path).1).last().copied().unwrap_or("")
}
fn split_suffix(name: &str) -> (&str, &str) {
match name.rfind('.') {
Some(i) if i > 0 && i + 1 < name.len() => (&name[..i], &name[i..]),
_ => (name, ""),
}
}
fn dirname_of(path: &str) -> String {
let (root, rest) = split_root(path);
let parts = components(rest);
let kept = &parts[..parts.len().saturating_sub(1)];
if kept.is_empty() {
if root.is_empty() {
".".to_string()
} else {
root
}
} else {
format!("{root}{}", kept.join("/"))
}
}
fn normalize_text(path: &str) -> String {
let (root, rest) = split_root(path);
let mut out: Vec<&str> = Vec::new();
for part in components(rest) {
if part != ".." {
out.push(part);
} else if out.last().is_some_and(|top| *top != "..") {
out.pop();
} else if root.is_empty() {
out.push("..");
}
}
let body = out.join("/");
if root.is_empty() && body.is_empty() {
".".to_string()
} else {
format!("{root}{body}")
}
}
fn join_text(parts: &[String]) -> String {
let mut out = String::new();
for part in parts {
let part = part.replace('\\', "/");
if !split_root(&part).0.is_empty() {
out = part;
} else if out.is_empty() || out.ends_with('/') {
out.push_str(&part);
} else {
out.push('/');
out.push_str(&part);
}
}
out
}
fn split_text(path: &str) -> Vec<String> {
let (root, rest) = split_root(path);
let mut out = Vec::new();
if !root.is_empty() {
out.push(root);
}
out.extend(components(rest).into_iter().map(str::to_string));
out
}
fn text_of(builtin: &str, value: &Value) -> Result<String> {
match value {
Value::Str(s) | Value::Uri(s) => Ok(s.clone()),
other => Err(crate::safety::bad_arg(
builtin,
"a path String",
other.type_name(),
)),
}
}
fn one_path(builtin: &str, args: Vec<Value>, input: Option<Value>) -> Result<String> {
let values: Vec<Value> = input.into_iter().chain(args).collect();
match values.as_slice() {
[value] => text_of(builtin, value),
_ => Err(crate::safety::bad_arg(
builtin,
"one path String, passed or piped",
&format!("{} values", values.len()),
)),
}
}
fn join_parts(builtin: &str, args: Vec<Value>, input: Option<Value>) -> Result<Vec<String>> {
let mut values: Vec<Value> = input.into_iter().chain(args).collect();
let expanded = match values.as_slice() {
[Value::Array(items)] => Some(items.clone()),
_ => None,
};
if let Some(items) = expanded {
values = items;
}
if values.is_empty() {
return Err(crate::safety::bad_arg(
builtin,
"at least one path String",
"no paths",
));
}
values.iter().map(|v| text_of(builtin, v)).collect()
}
pub fn bi_path_join(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "path_join";
let parts = join_parts(B, args, input)?;
Ok(Value::Str(join_text(&parts)))
}
pub fn bi_path_basename(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "path_basename";
let path = one_path(B, args, input)?;
Ok(Value::Str(name_of(&path).to_string()))
}
pub fn bi_path_dirname(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "path_dirname";
let path = one_path(B, args, input)?;
Ok(Value::Str(dirname_of(&path)))
}
pub fn bi_path_extension(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "path_extension";
let path = one_path(B, args, input)?;
Ok(Value::Str(split_suffix(name_of(&path)).1.to_string()))
}
pub fn bi_path_stem(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "path_stem";
let path = one_path(B, args, input)?;
Ok(Value::Str(split_suffix(name_of(&path)).0.to_string()))
}
pub fn bi_path_normalize(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "path_normalize";
let path = one_path(B, args, input)?;
Ok(Value::Str(normalize_text(&path)))
}
pub fn bi_path_is_absolute(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "path_is_absolute";
let path = one_path(B, args, input)?;
Ok(Value::Bool(!split_root(&path).0.is_empty()))
}
pub fn bi_path_split(args: Vec<Value>, input: Option<Value>) -> Result<Value> {
const B: &str = "path_split";
let path = one_path(B, args, input)?;
Ok(Value::Array(
split_text(&path).into_iter().map(Value::Str).collect(),
))
}