1use crate::error::ExpressionError;
11use crate::function_library::EvalContext;
12use crate::path_mapping::PathFormat;
13use crate::value::ExprValue;
14
15use super::path_parse as pp;
16
17type R = Result<ExprValue, ExpressionError>;
18type Ctx<'a> = &'a mut dyn EvalContext;
19
20fn get_path(a: &ExprValue, ctx: &dyn EvalContext) -> Result<(String, PathFormat), ExpressionError> {
21 match a {
22 ExprValue::Path { value, format } => Ok((value.clone(), *format)),
23 ExprValue::String(s) => Ok((s.clone(), ctx.path_format())),
24 _ => Err(ExpressionError::new(format!(
25 "Path method not supported on {}",
26 a.expr_type()
27 ))),
28 }
29}
30
31fn get_str_arg(a: &[ExprValue], idx: usize) -> String {
32 a.get(idx)
33 .map(|v| match v {
34 ExprValue::String(s) => s.clone(),
35 ExprValue::Path { value, .. } => value.clone(),
36 _ => String::new(),
37 })
38 .unwrap_or_default()
39}
40
41pub fn as_posix_fn(ctx: Ctx, a: &[ExprValue]) -> R {
42 let (path_str, _) = get_path(&a[0], ctx)?;
43 Ok(ExprValue::String(path_str.replace('\\', "/")))
44}
45
46pub fn with_name_fn(ctx: Ctx, a: &[ExprValue]) -> R {
47 let (path_str, fmt) = get_path(&a[0], ctx)?;
48 let new_name = get_str_arg(a, 1);
49 if new_name.contains('/') || (fmt == PathFormat::Windows && new_name.contains('\\')) {
50 return Err(ExpressionError::new(format!(
51 "with_name: name must not contain path separators, got '{new_name}'"
52 )));
53 }
54 let parent = pp::parent(&path_str, fmt);
55 let sep = pp::sep(fmt);
56 Ok(ExprValue::new_path(format!("{parent}{sep}{new_name}"), fmt))
57}
58
59pub fn with_stem_fn(ctx: Ctx, a: &[ExprValue]) -> R {
60 let (path_str, fmt) = get_path(&a[0], ctx)?;
61 let new_stem = get_str_arg(a, 1);
62 if new_stem.contains('/') || (fmt == PathFormat::Windows && new_stem.contains('\\')) {
63 return Err(ExpressionError::new(format!(
64 "with_stem: name must not contain path separators, got '{new_stem}'"
65 )));
66 }
67 let ext = pp::extension(&path_str, fmt);
68 let parent = pp::parent(&path_str, fmt);
69 let sep = pp::sep(fmt);
70 Ok(ExprValue::new_path(
71 format!("{parent}{sep}{new_stem}{ext}"),
72 fmt,
73 ))
74}
75
76pub fn with_suffix_fn(ctx: Ctx, a: &[ExprValue]) -> R {
77 let (path_str, fmt) = get_path(&a[0], ctx)?;
78 let new_suffix = get_str_arg(a, 1);
79 ctx.count_string_ops(path_str.len())?;
80 if crate::uri_path::is_uri(&path_str) {
81 let stem = crate::uri_path::stem(&path_str);
82 let parent = crate::uri_path::parent(&path_str);
83 return Ok(ExprValue::new_path(
84 format!("{parent}/{stem}{new_suffix}"),
85 fmt,
86 ));
87 }
88 let stem = pp::file_stem(&path_str, fmt);
89 let parent = pp::parent(&path_str, fmt);
90 let sep = pp::sep(fmt);
91 Ok(ExprValue::new_path(
92 format!("{parent}{sep}{stem}{new_suffix}"),
93 fmt,
94 ))
95}
96
97pub fn with_number_fn(ctx: Ctx, a: &[ExprValue]) -> R {
98 let (path_str, fmt) = get_path(&a[0], ctx)?;
99 let num = match &a[1] {
100 ExprValue::Int(n) => *n,
101 _ => return Err(ExpressionError::new("with_number() requires int argument")),
102 };
103 let is_string = matches!(&a[0], ExprValue::String(_));
104 let (dir_part, filename) = pp::split(&path_str, fmt);
105 let prefix = if dir_part.is_empty() {
106 String::new()
107 } else {
108 format!("{}{}", dir_part, pp::sep(fmt))
109 };
110 let (stem, suffix) = match filename.rfind('.') {
111 Some(i) if i > 0 => (&filename[..i], &filename[i..]),
112 _ => (filename, ""),
113 };
114 let new_stem = with_number_replace(stem, num)?;
115 let result = format!("{prefix}{new_stem}{suffix}");
116 if is_string {
117 Ok(ExprValue::String(result))
118 } else {
119 Ok(ExprValue::new_path(result, fmt))
120 }
121}
122
123pub fn is_absolute_fn(ctx: Ctx, a: &[ExprValue]) -> R {
124 let (path_str, fmt) = get_path(&a[0], ctx)?;
125 Ok(ExprValue::Bool(is_absolute(&path_str, fmt)))
126}
127
128pub fn is_absolute(path_str: &str, fmt: PathFormat) -> bool {
130 if crate::uri_path::is_uri(path_str) {
131 return true;
132 }
133 let bytes = path_str.as_bytes();
134 if bytes.len() >= 2
136 && ((bytes[0] == b'/' && bytes[1] == b'/') || (bytes[0] == b'\\' && bytes[1] == b'\\'))
137 {
138 return true;
139 }
140 match fmt {
141 PathFormat::Windows => {
142 bytes.len() >= 3
143 && bytes[0].is_ascii_alphabetic()
144 && bytes[1] == b':'
145 && (bytes[2] == b'\\' || bytes[2] == b'/')
146 }
147 PathFormat::Posix | PathFormat::Uri => bytes.first() == Some(&b'/'),
148 }
149}
150
151pub fn join(left: &str, right: &str, fmt: PathFormat) -> String {
158 if is_absolute(right, fmt) {
159 return right.to_string();
160 }
161 if fmt == PathFormat::Windows {
166 let rb = right.as_bytes();
167 if rb.first() == Some(&b'/') || rb.first() == Some(&b'\\') {
168 let lb = left.as_bytes();
169 if lb.len() >= 2 && lb[0].is_ascii_alphabetic() && lb[1] == b':' {
171 return format!("{}{right}", &left[..2]);
172 }
173 if let Some(unc_root) = extract_unc_root(left) {
175 return format!("{unc_root}{right}");
176 }
177 }
178 }
179 let left_is_uri = crate::uri_path::is_uri(left);
180 let (sep, trim_chars): (&str, &[char]) = if left_is_uri {
181 ("/", &['/'])
182 } else {
183 match fmt {
184 PathFormat::Windows => ("\\", &['/', '\\']),
186 PathFormat::Posix | PathFormat::Uri => ("/", &['/']),
188 }
189 };
190 let left = left.trim_end_matches(trim_chars);
191 let right = if left_is_uri && fmt == PathFormat::Windows {
194 std::borrow::Cow::Owned(right.replace('\\', "/"))
195 } else {
196 std::borrow::Cow::Borrowed(right)
197 };
198 format!("{left}{sep}{right}")
199}
200
201pub fn non_uri_join(left: &str, right: &str, fmt: PathFormat) -> String {
208 if fmt == PathFormat::Windows {
210 let rb = right.as_bytes();
211 if rb.first() == Some(&b'/') || rb.first() == Some(&b'\\') {
212 let lb = left.as_bytes();
213 if lb.len() >= 2 && lb[0].is_ascii_alphabetic() && lb[1] == b':' {
214 return format!("{}{right}", &left[..2]);
215 }
216 if let Some(unc_root) = extract_unc_root(left) {
217 return format!("{unc_root}{right}");
218 }
219 }
220 }
221 let (sep, trim_chars): (&str, &[char]) = match fmt {
222 PathFormat::Windows => ("\\", &['/', '\\']),
223 PathFormat::Posix | PathFormat::Uri => ("/", &['/']),
224 };
225 let left = left.trim_end_matches(trim_chars);
226 format!("{left}{sep}{right}")
227}
228
229fn extract_unc_root(path: &str) -> Option<&str> {
232 let bytes = path.as_bytes();
233 if bytes.len() < 2 {
234 return None;
235 }
236 let prefix_char = bytes[0];
237 if !((prefix_char == b'\\' && bytes[1] == b'\\') || (prefix_char == b'/' && bytes[1] == b'/')) {
238 return None;
239 }
240 let rest = &path[2..];
242 let sep_after_server = rest.find(['/', '\\'])?;
243 let after_server = sep_after_server + 3; let share_start = after_server;
246 let sep_after_share = path[share_start..]
247 .find(['/', '\\'])
248 .map(|i| share_start + i)
249 .unwrap_or(path.len());
250 Some(&path[..sep_after_share])
251}
252
253fn path_starts_with(path: &str, base: &str, fmt: PathFormat) -> bool {
254 if fmt == PathFormat::Windows {
255 path.len() >= base.len() && path[..base.len()].eq_ignore_ascii_case(base)
256 } else {
257 path.starts_with(base)
258 }
259}
260
261pub fn is_relative_to_fn(ctx: Ctx, a: &[ExprValue]) -> R {
262 let (path_str, fmt) = get_path(&a[0], ctx)?;
263 let base = get_str_arg(a, 1);
264 let is_rel = path_starts_with(&path_str, &base, fmt)
265 && (path_str.len() == base.len()
266 || base.ends_with('/')
267 || base.ends_with('\\')
268 || matches!(path_str.as_bytes().get(base.len()), Some(b'/' | b'\\')));
269 Ok(ExprValue::Bool(is_rel))
270}
271
272pub fn relative_to_fn(ctx: Ctx, a: &[ExprValue]) -> R {
273 let (path_str, fmt) = get_path(&a[0], ctx)?;
274 let base = get_str_arg(a, 1);
275 let is_rel = path_starts_with(&path_str, &base, fmt)
276 && (path_str.len() == base.len()
277 || base.ends_with('/')
278 || base.ends_with('\\')
279 || matches!(path_str.as_bytes().get(base.len()), Some(b'/' | b'\\')));
280 if !is_rel {
281 return Err(ExpressionError::new(format!(
282 "relative_to failed: '{path_str}' is not relative to '{base}'"
283 )));
284 }
285 let rel = path_str[base.len()..]
286 .trim_start_matches('/')
287 .trim_start_matches('\\');
288 Ok(ExprValue::new_path(
289 if rel.is_empty() {
290 ".".to_string()
291 } else {
292 rel.to_string()
293 },
294 fmt,
295 ))
296}
297
298pub fn make_apply_path_mapping_fn(
306 rules: std::sync::Arc<Vec<crate::path_mapping::PathMappingRule>>,
307) -> impl Fn(&mut dyn EvalContext, &[ExprValue]) -> R + Send + Sync + 'static {
308 move |ctx, a| {
309 let (path_str, fmt) = get_path(&a[0], ctx)?;
310 let mapped =
311 crate::path_mapping::apply_rules_with_format(&rules, &path_str, ctx.path_format());
312 if mapped == path_str {
313 Ok(ExprValue::new_path(path_str, fmt))
314 } else {
315 Ok(ExprValue::new_path(mapped, fmt))
316 }
317 }
318}
319
320fn format_padded(num: i64, width: usize) -> String {
321 if num < 0 {
322 format!("-{:0>width$}", -num, width = width.saturating_sub(1))
323 } else {
324 format!("{:0>width$}", num, width = width)
325 }
326}
327
328const MAX_PADDING_WIDTH: usize = 32;
329
330fn with_number_replace(stem: &str, num: i64) -> Result<String, ExpressionError> {
331 if let Some(pct) = stem.rfind('%') {
333 let after = &stem[pct + 1..];
334 if after == "d" {
335 return Ok(format!("{}{}", &stem[..pct], num));
336 }
337 if after.starts_with('0') && after.ends_with('d') {
338 let width: usize = after[1..after.len() - 1].parse().unwrap_or(1);
339 if width > MAX_PADDING_WIDTH {
340 return Err(ExpressionError::new(format!(
341 "with_number: padding width {width} exceeds maximum of {MAX_PADDING_WIDTH}"
342 )));
343 }
344 return Ok(format!("{}{}", &stem[..pct], format_padded(num, width)));
345 }
346 }
347 if let Some(start) = stem.rfind('#') {
349 let hash_start = stem[..=start]
350 .rfind(|c: char| c != '#')
351 .map(|i| i + 1)
352 .unwrap_or(0);
353 let width = start - hash_start + 1;
354 if width > MAX_PADDING_WIDTH {
355 return Err(ExpressionError::new(format!(
356 "with_number: padding width {width} exceeds maximum of {MAX_PADDING_WIDTH}"
357 )));
358 }
359 return Ok(format!(
360 "{}{}",
361 &stem[..hash_start],
362 format_padded(num, width)
363 ));
364 }
365 let digit_start = stem.len()
367 - stem
368 .chars()
369 .rev()
370 .take_while(|c| c.is_ascii_digit())
371 .count();
372 if digit_start < stem.len() {
373 let width = stem.len() - digit_start;
374 return Ok(format!(
375 "{}{}",
376 &stem[..digit_start],
377 format_padded(num, width)
378 ));
379 }
380 Ok(format!("{}_{}", stem, format_padded(num, 4)))
382}
383
384pub fn prop_name(ctx: Ctx, a: &[ExprValue]) -> R {
387 let (path_str, fmt) = get_path(&a[0], ctx)?;
388 ctx.count_string_ops(path_str.len())?;
389 if crate::uri_path::is_uri(&path_str) {
390 return Ok(ExprValue::String(crate::uri_path::name(&path_str)));
391 }
392 Ok(ExprValue::String(pp::file_name(&path_str, fmt).to_string()))
393}
394
395pub fn prop_stem(ctx: Ctx, a: &[ExprValue]) -> R {
396 let (path_str, fmt) = get_path(&a[0], ctx)?;
397 ctx.count_string_ops(path_str.len())?;
398 if crate::uri_path::is_uri(&path_str) {
399 return Ok(ExprValue::String(crate::uri_path::stem(&path_str)));
400 }
401 Ok(ExprValue::String(pp::file_stem(&path_str, fmt).to_string()))
402}
403
404pub fn prop_suffix(ctx: Ctx, a: &[ExprValue]) -> R {
405 let (path_str, fmt) = get_path(&a[0], ctx)?;
406 ctx.count_string_ops(path_str.len())?;
407 Ok(ExprValue::String(if crate::uri_path::is_uri(&path_str) {
408 crate::uri_path::suffix(&path_str)
409 } else {
410 pp::extension(&path_str, fmt).to_string()
411 }))
412}
413
414pub fn prop_suffixes(ctx: Ctx, a: &[ExprValue]) -> R {
415 let (path_str, fmt) = get_path(&a[0], ctx)?;
416 ctx.count_string_ops(path_str.len())?;
417 if crate::uri_path::is_uri(&path_str) {
418 let suffixes: Vec<ExprValue> = crate::uri_path::suffixes(&path_str)
419 .into_iter()
420 .map(ExprValue::String)
421 .collect();
422 return ExprValue::make_list_checked(ctx, suffixes, crate::types::ExprType::STRING);
423 }
424 let suffixes: Vec<ExprValue> = pp::suffixes(&path_str, fmt)
425 .into_iter()
426 .map(ExprValue::String)
427 .collect();
428 ExprValue::make_list_checked(ctx, suffixes, crate::types::ExprType::STRING)
429}
430
431pub fn prop_parent(ctx: Ctx, a: &[ExprValue]) -> R {
432 let (path_str, fmt) = get_path(&a[0], ctx)?;
433 ctx.count_string_ops(path_str.len())?;
434 if crate::uri_path::is_uri(&path_str) {
435 return Ok(ExprValue::new_path(crate::uri_path::parent(&path_str), fmt));
436 }
437 Ok(ExprValue::new_path(pp::parent(&path_str, fmt), fmt))
438}
439
440pub fn prop_parts(ctx: Ctx, a: &[ExprValue]) -> R {
441 let (path_str, fmt) = get_path(&a[0], ctx)?;
442 ctx.count_string_ops(path_str.len())?;
443 if crate::uri_path::is_uri(&path_str) {
444 let parts: Vec<ExprValue> = crate::uri_path::parts(&path_str)
445 .into_iter()
446 .map(ExprValue::String)
447 .collect();
448 return ExprValue::make_list_checked(ctx, parts, crate::types::ExprType::STRING);
449 }
450 let parts: Vec<ExprValue> = pp::parts(&path_str, fmt)
451 .into_iter()
452 .map(ExprValue::String)
453 .collect();
454 ExprValue::make_list_checked(ctx, parts, crate::types::ExprType::STRING)
455}