use crate::error::ExpressionError;
use crate::function_library::EvalContext;
use crate::path_mapping::PathFormat;
use crate::value::ExprValue;
use super::path_parse as pp;
type R = Result<ExprValue, ExpressionError>;
type Ctx<'a> = &'a mut dyn EvalContext;
fn get_path(a: &ExprValue, ctx: &dyn EvalContext) -> Result<(String, PathFormat), ExpressionError> {
match a {
ExprValue::Path { value, format } => Ok((value.clone(), *format)),
ExprValue::String(s) => Ok((s.clone(), ctx.path_format())),
_ => Err(ExpressionError::new(format!(
"Path method not supported on {}",
a.expr_type()
))),
}
}
fn get_str_arg(a: &[ExprValue], idx: usize) -> String {
a.get(idx)
.map(|v| match v {
ExprValue::String(s) => s.clone(),
ExprValue::Path { value, .. } => value.clone(),
_ => String::new(),
})
.unwrap_or_default()
}
pub fn as_posix_fn(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, _) = get_path(&a[0], ctx)?;
ctx.count_string_ops(path_str.len())?;
Ok(ExprValue::String(path_str.replace('\\', "/")))
}
fn has_empty_name(path_str: &str, fmt: PathFormat) -> bool {
if crate::uri_path::is_uri(path_str) {
crate::uri_path::name(path_str).is_empty()
} else {
pp::file_name(path_str, fmt).is_empty()
}
}
fn is_valid_name(name: &str, fmt: PathFormat) -> bool {
if name.is_empty() || name == "." {
return false;
}
if name.contains('/') {
return false;
}
if fmt == PathFormat::Windows && name.contains('\\') {
return false;
}
true
}
fn is_valid_suffix(suffix: &str, fmt: PathFormat) -> bool {
if suffix.is_empty() {
return true;
}
if !suffix.starts_with('.') || suffix == "." {
return false;
}
if suffix.contains('/') {
return false;
}
if fmt == PathFormat::Windows && suffix.contains('\\') {
return false;
}
true
}
fn join_parent_and_name(parent: &str, name: &str, fmt: PathFormat) -> String {
if parent.is_empty() || parent == "." {
if fmt == PathFormat::Windows {
let nb = name.as_bytes();
if nb.len() >= 2 && nb[0].is_ascii_alphabetic() && nb[1] == b':' {
return format!(".\\{name}");
}
}
return name.to_string();
}
let last = parent.as_bytes().last().copied();
let already_terminated = match fmt {
PathFormat::Windows => {
if last == Some(b'/') || last == Some(b'\\') {
true
} else {
let bytes = parent.as_bytes();
bytes.len() == 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
}
}
PathFormat::Posix | PathFormat::Uri => last == Some(b'/'),
};
if already_terminated {
format!("{parent}{name}")
} else {
format!("{parent}{sep}{name}", sep = pp::sep(fmt))
}
}
pub fn with_name_fn(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
let new_name = get_str_arg(a, 1);
if !is_valid_name(&new_name, fmt) {
return Err(ExpressionError::new(format!(
"with_name: Invalid name '{new_name}'"
)));
}
ctx.count_string_ops(path_str.len())?;
if has_empty_name(&path_str, fmt) {
return Err(ExpressionError::new(format!(
"with_name: '{path_str}' has an empty name"
)));
}
if crate::uri_path::is_uri(&path_str) {
let parent = crate::uri_path::parent(&path_str);
return Ok(ExprValue::new_path(format!("{parent}/{new_name}"), fmt));
}
let parent = pp::parent(&path_str, fmt);
Ok(ExprValue::new_path(
join_parent_and_name(&parent, &new_name, fmt),
fmt,
))
}
pub fn with_stem_fn(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
let new_stem = get_str_arg(a, 1);
if !new_stem.is_empty() && new_stem != "." {
if new_stem.contains('/') || (fmt == PathFormat::Windows && new_stem.contains('\\')) {
return Err(ExpressionError::new(format!(
"with_stem: Invalid name '{new_stem}'"
)));
}
}
ctx.count_string_ops(path_str.len())?;
if has_empty_name(&path_str, fmt) {
return Err(ExpressionError::new(format!(
"with_stem: '{path_str}' has an empty name"
)));
}
let is_uri = crate::uri_path::is_uri(&path_str);
let (parent, ext) = if is_uri {
(
crate::uri_path::parent(&path_str),
crate::uri_path::suffix(&path_str),
)
} else {
(
pp::parent(&path_str, fmt),
pp::extension(&path_str, fmt).to_string(),
)
};
if new_stem.is_empty() && !ext.is_empty() {
return Err(ExpressionError::new(format!(
"with_stem: '{path_str}' has a non-empty suffix"
)));
}
let new_filename = format!("{new_stem}{ext}");
if !is_valid_name(&new_filename, fmt) {
return Err(ExpressionError::new(format!(
"with_stem: Invalid name '{new_filename}'"
)));
}
let result = if is_uri {
format!("{parent}/{new_filename}")
} else {
join_parent_and_name(&parent, &new_filename, fmt)
};
Ok(ExprValue::new_path(result, fmt))
}
pub fn with_suffix_fn(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
let new_suffix = get_str_arg(a, 1);
if !is_valid_suffix(&new_suffix, fmt) {
return Err(ExpressionError::new(format!(
"with_suffix: Invalid suffix '{new_suffix}'"
)));
}
ctx.count_string_ops(path_str.len())?;
if has_empty_name(&path_str, fmt) {
return Err(ExpressionError::new(format!(
"with_suffix: '{path_str}' has an empty name"
)));
}
let is_uri = crate::uri_path::is_uri(&path_str);
let (parent, stem) = if is_uri {
(
crate::uri_path::parent(&path_str),
crate::uri_path::stem(&path_str),
)
} else {
(
pp::parent(&path_str, fmt),
pp::file_stem(&path_str, fmt).to_string(),
)
};
let new_filename = format!("{stem}{new_suffix}");
if !is_valid_name(&new_filename, fmt) {
return Err(ExpressionError::new(format!(
"with_suffix: Invalid name '{new_filename}'"
)));
}
let result = if is_uri {
format!("{parent}/{new_filename}")
} else {
join_parent_and_name(&parent, &new_filename, fmt)
};
Ok(ExprValue::new_path(result, fmt))
}
fn split_name_at_suffix(filename: &str) -> (&str, &str) {
match filename.rfind('.') {
Some(i) if i > 0 && i + 1 < filename.len() => (&filename[..i], &filename[i..]),
_ => (filename, ""),
}
}
pub fn with_number_fn(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
let num = match &a[1] {
ExprValue::Int(n) => *n,
_ => return Err(ExpressionError::new("with_number() requires int argument")),
};
ctx.count_string_ops(path_str.len())?;
if has_empty_name(&path_str, fmt) {
return Err(ExpressionError::new(format!(
"with_number: '{path_str}' has an empty name"
)));
}
let is_string = matches!(&a[0], ExprValue::String(_));
let result = if crate::uri_path::is_uri(&path_str) {
let parent = crate::uri_path::parent(&path_str);
let filename = crate::uri_path::name(&path_str);
let (stem, suffix) = split_name_at_suffix(&filename);
let new_stem = with_number_replace(stem, num)?;
format!("{parent}/{new_stem}{suffix}")
} else {
let (dir_part, filename) = pp::split(&path_str, fmt);
let (stem, suffix) = split_name_at_suffix(filename);
let new_stem = with_number_replace(stem, num)?;
let new_filename = format!("{new_stem}{suffix}");
join_parent_and_name(dir_part, &new_filename, fmt)
};
if is_string {
Ok(ExprValue::String(result))
} else {
Ok(ExprValue::new_path(result, fmt))
}
}
pub fn is_absolute_fn(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
Ok(ExprValue::Bool(is_absolute(&path_str, fmt)))
}
pub fn is_absolute(path_str: &str, fmt: PathFormat) -> bool {
if crate::uri_path::is_uri(path_str) {
return true;
}
let bytes = path_str.as_bytes();
if bytes.len() >= 2
&& ((bytes[0] == b'/' && bytes[1] == b'/') || (bytes[0] == b'\\' && bytes[1] == b'\\'))
{
return true;
}
match fmt {
PathFormat::Windows => {
bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'\\' || bytes[2] == b'/')
}
PathFormat::Posix | PathFormat::Uri => bytes.first() == Some(&b'/'),
}
}
pub fn join(left: &str, right: &str, fmt: PathFormat) -> String {
if is_absolute(right, fmt) {
return right.to_string();
}
if fmt == PathFormat::Windows {
let rb = right.as_bytes();
if rb.first() == Some(&b'/') || rb.first() == Some(&b'\\') {
let lb = left.as_bytes();
if lb.len() >= 2 && lb[0].is_ascii_alphabetic() && lb[1] == b':' {
return format!("{}{right}", &left[..2]);
}
if let Some(unc_root) = extract_unc_root(left) {
return format!("{unc_root}{right}");
}
}
}
let left_is_uri = crate::uri_path::is_uri(left);
let (sep, trim_chars): (&str, &[char]) = if left_is_uri {
("/", &['/'])
} else {
match fmt {
PathFormat::Windows => ("\\", &['/', '\\']),
PathFormat::Posix | PathFormat::Uri => ("/", &['/']),
}
};
let left = left.trim_end_matches(trim_chars);
let right = if left_is_uri && fmt == PathFormat::Windows {
std::borrow::Cow::Owned(right.replace('\\', "/"))
} else {
std::borrow::Cow::Borrowed(right)
};
format!("{left}{sep}{right}")
}
pub fn non_uri_join(left: &str, right: &str, fmt: PathFormat) -> String {
if fmt == PathFormat::Windows {
let rb = right.as_bytes();
if rb.first() == Some(&b'/') || rb.first() == Some(&b'\\') {
let lb = left.as_bytes();
if lb.len() >= 2 && lb[0].is_ascii_alphabetic() && lb[1] == b':' {
return format!("{}{right}", &left[..2]);
}
if let Some(unc_root) = extract_unc_root(left) {
return format!("{unc_root}{right}");
}
}
}
let (sep, trim_chars): (&str, &[char]) = match fmt {
PathFormat::Windows => ("\\", &['/', '\\']),
PathFormat::Posix | PathFormat::Uri => ("/", &['/']),
};
let left = left.trim_end_matches(trim_chars);
format!("{left}{sep}{right}")
}
fn extract_unc_root(path: &str) -> Option<&str> {
let bytes = path.as_bytes();
if bytes.len() < 2 {
return None;
}
let prefix_char = bytes[0];
if !((prefix_char == b'\\' && bytes[1] == b'\\') || (prefix_char == b'/' && bytes[1] == b'/')) {
return None;
}
let rest = &path[2..];
let sep_after_server = rest.find(['/', '\\'])?;
let after_server = sep_after_server + 3; let share_start = after_server;
let sep_after_share = path[share_start..]
.find(['/', '\\'])
.map(|i| share_start + i)
.unwrap_or(path.len());
Some(&path[..sep_after_share])
}
fn path_starts_with(path: &str, base: &str, fmt: PathFormat) -> bool {
if fmt == PathFormat::Windows {
path.len() >= base.len() && path[..base.len()].eq_ignore_ascii_case(base)
} else {
path.starts_with(base)
}
}
pub fn is_relative_to_fn(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
let base = get_str_arg(a, 1);
ctx.count_string_ops(path_str.len().max(base.len()))?;
let is_rel = path_starts_with(&path_str, &base, fmt)
&& (path_str.len() == base.len()
|| base.ends_with('/')
|| base.ends_with('\\')
|| matches!(path_str.as_bytes().get(base.len()), Some(b'/' | b'\\')));
Ok(ExprValue::Bool(is_rel))
}
pub fn relative_to_fn(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
let base = get_str_arg(a, 1);
ctx.count_string_ops(path_str.len().max(base.len()))?;
let is_rel = path_starts_with(&path_str, &base, fmt)
&& (path_str.len() == base.len()
|| base.ends_with('/')
|| base.ends_with('\\')
|| matches!(path_str.as_bytes().get(base.len()), Some(b'/' | b'\\')));
if !is_rel {
return Err(ExpressionError::new(format!(
"relative_to failed: '{path_str}' is not relative to '{base}'"
)));
}
let rel = path_str[base.len()..]
.trim_start_matches('/')
.trim_start_matches('\\');
Ok(ExprValue::new_path(
if rel.is_empty() {
".".to_string()
} else {
rel.to_string()
},
fmt,
))
}
pub fn make_apply_path_mapping_fn(
rules: std::sync::Arc<Vec<crate::path_mapping::PathMappingRule>>,
) -> impl Fn(&mut dyn EvalContext, &[ExprValue]) -> R + Send + Sync + 'static {
move |ctx, a| {
let (path_str, fmt) = get_path(&a[0], ctx)?;
ctx.count_string_ops(path_str.len())?;
let mapped =
crate::path_mapping::apply_rules_with_format(&rules, &path_str, ctx.path_format());
if mapped == path_str {
Ok(ExprValue::new_path(path_str, fmt))
} else {
Ok(ExprValue::new_path(mapped, fmt))
}
}
}
fn format_padded(num: i64, width: usize) -> String {
if num < 0 {
format!("-{:0>width$}", -num, width = width.saturating_sub(1))
} else {
format!("{:0>width$}", num, width = width)
}
}
const MAX_PADDING_WIDTH: usize = 32;
fn with_number_replace(stem: &str, num: i64) -> Result<String, ExpressionError> {
if let Some(pct) = stem.rfind('%') {
let after = &stem[pct + 1..];
if after == "d" {
return Ok(format!("{}{}", &stem[..pct], num));
}
if after.starts_with('0') && after.ends_with('d') {
let width: usize = after[1..after.len() - 1].parse().unwrap_or(1);
if width > MAX_PADDING_WIDTH {
return Err(ExpressionError::new(format!(
"with_number: padding width {width} exceeds maximum of {MAX_PADDING_WIDTH}"
)));
}
return Ok(format!("{}{}", &stem[..pct], format_padded(num, width)));
}
}
if let Some(start) = stem.rfind('#') {
let hash_start = stem[..=start]
.rfind(|c: char| c != '#')
.map(|i| i + 1)
.unwrap_or(0);
let width = start - hash_start + 1;
if width > MAX_PADDING_WIDTH {
return Err(ExpressionError::new(format!(
"with_number: padding width {width} exceeds maximum of {MAX_PADDING_WIDTH}"
)));
}
return Ok(format!(
"{}{}",
&stem[..hash_start],
format_padded(num, width)
));
}
let digit_start = stem.len()
- stem
.chars()
.rev()
.take_while(|c| c.is_ascii_digit())
.count();
if digit_start < stem.len() {
let width = stem.len() - digit_start;
return Ok(format!(
"{}{}",
&stem[..digit_start],
format_padded(num, width)
));
}
Ok(format!("{}_{}", stem, format_padded(num, 4)))
}
pub fn prop_name(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
ctx.count_string_ops(path_str.len())?;
if crate::uri_path::is_uri(&path_str) {
return Ok(ExprValue::String(crate::uri_path::name(&path_str)));
}
Ok(ExprValue::String(pp::file_name(&path_str, fmt).to_string()))
}
pub fn prop_stem(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
ctx.count_string_ops(path_str.len())?;
if crate::uri_path::is_uri(&path_str) {
return Ok(ExprValue::String(crate::uri_path::stem(&path_str)));
}
Ok(ExprValue::String(pp::file_stem(&path_str, fmt).to_string()))
}
pub fn prop_suffix(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
ctx.count_string_ops(path_str.len())?;
Ok(ExprValue::String(if crate::uri_path::is_uri(&path_str) {
crate::uri_path::suffix(&path_str)
} else {
pp::extension(&path_str, fmt).to_string()
}))
}
pub fn prop_suffixes(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
ctx.count_string_ops(path_str.len())?;
if crate::uri_path::is_uri(&path_str) {
let suffixes: Vec<ExprValue> = crate::uri_path::suffixes(&path_str)
.into_iter()
.map(ExprValue::String)
.collect();
return ExprValue::make_list_checked(ctx, suffixes, crate::types::ExprType::STRING);
}
let suffixes: Vec<ExprValue> = pp::suffixes(&path_str, fmt)
.into_iter()
.map(ExprValue::String)
.collect();
ExprValue::make_list_checked(ctx, suffixes, crate::types::ExprType::STRING)
}
pub fn prop_parent(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
ctx.count_string_ops(path_str.len())?;
if crate::uri_path::is_uri(&path_str) {
return Ok(ExprValue::new_path(crate::uri_path::parent(&path_str), fmt));
}
Ok(ExprValue::new_path(pp::parent(&path_str, fmt), fmt))
}
pub fn prop_parts(ctx: Ctx, a: &[ExprValue]) -> R {
let (path_str, fmt) = get_path(&a[0], ctx)?;
ctx.count_string_ops(path_str.len())?;
if crate::uri_path::is_uri(&path_str) {
let parts: Vec<ExprValue> = crate::uri_path::parts(&path_str)
.into_iter()
.map(ExprValue::String)
.collect();
return ExprValue::make_list_checked(ctx, parts, crate::types::ExprType::STRING);
}
let parts: Vec<ExprValue> = pp::parts(&path_str, fmt)
.into_iter()
.map(ExprValue::String)
.collect();
ExprValue::make_list_checked(ctx, parts, crate::types::ExprType::STRING)
}