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 ctx.count_string_ops(path_str.len())?;
47 Ok(ExprValue::String(path_str.replace('\\', "/")))
48}
49
50fn has_empty_name(path_str: &str, fmt: PathFormat) -> bool {
68 if crate::uri_path::is_uri(path_str) {
69 crate::uri_path::name(path_str).is_empty()
70 } else {
71 pp::file_name(path_str, fmt).is_empty()
72 }
73}
74
75fn is_valid_name(name: &str, fmt: PathFormat) -> bool {
85 if name.is_empty() || name == "." {
86 return false;
87 }
88 if name.contains('/') {
89 return false;
90 }
91 if fmt == PathFormat::Windows && name.contains('\\') {
92 return false;
93 }
94 true
95}
96
97fn is_valid_suffix(suffix: &str, fmt: PathFormat) -> bool {
107 if suffix.is_empty() {
108 return true;
109 }
110 if !suffix.starts_with('.') || suffix == "." {
111 return false;
112 }
113 if suffix.contains('/') {
114 return false;
115 }
116 if fmt == PathFormat::Windows && suffix.contains('\\') {
117 return false;
118 }
119 true
120}
121
122fn join_parent_and_name(parent: &str, name: &str, fmt: PathFormat) -> String {
135 if parent.is_empty() || parent == "." {
136 if fmt == PathFormat::Windows {
155 let nb = name.as_bytes();
156 if nb.len() >= 2 && nb[0].is_ascii_alphabetic() && nb[1] == b':' {
157 return format!(".\\{name}");
158 }
159 }
160 return name.to_string();
161 }
162 let last = parent.as_bytes().last().copied();
163 let already_terminated = match fmt {
164 PathFormat::Windows => {
165 if last == Some(b'/') || last == Some(b'\\') {
168 true
169 } else {
170 let bytes = parent.as_bytes();
176 bytes.len() == 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
177 }
178 }
179 PathFormat::Posix | PathFormat::Uri => last == Some(b'/'),
180 };
181 if already_terminated {
182 format!("{parent}{name}")
183 } else {
184 format!("{parent}{sep}{name}", sep = pp::sep(fmt))
185 }
186}
187
188pub fn with_name_fn(ctx: Ctx, a: &[ExprValue]) -> R {
189 let (path_str, fmt) = get_path(&a[0], ctx)?;
190 let new_name = get_str_arg(a, 1);
191 if !is_valid_name(&new_name, fmt) {
192 return Err(ExpressionError::new(format!(
193 "with_name: Invalid name '{new_name}'"
194 )));
195 }
196 ctx.count_string_ops(path_str.len())?;
203 if has_empty_name(&path_str, fmt) {
204 return Err(ExpressionError::new(format!(
205 "with_name: '{path_str}' has an empty name"
206 )));
207 }
208 if crate::uri_path::is_uri(&path_str) {
216 let parent = crate::uri_path::parent(&path_str);
217 return Ok(ExprValue::new_path(format!("{parent}/{new_name}"), fmt));
218 }
219 let parent = pp::parent(&path_str, fmt);
220 Ok(ExprValue::new_path(
221 join_parent_and_name(&parent, &new_name, fmt),
222 fmt,
223 ))
224}
225
226pub fn with_stem_fn(ctx: Ctx, a: &[ExprValue]) -> R {
227 let (path_str, fmt) = get_path(&a[0], ctx)?;
228 let new_stem = get_str_arg(a, 1);
229 if !new_stem.is_empty() && new_stem != "." {
246 if new_stem.contains('/') || (fmt == PathFormat::Windows && new_stem.contains('\\')) {
249 return Err(ExpressionError::new(format!(
250 "with_stem: Invalid name '{new_stem}'"
251 )));
252 }
253 }
254 ctx.count_string_ops(path_str.len())?;
255 if has_empty_name(&path_str, fmt) {
256 return Err(ExpressionError::new(format!(
257 "with_stem: '{path_str}' has an empty name"
258 )));
259 }
260 let is_uri = crate::uri_path::is_uri(&path_str);
266 let (parent, ext) = if is_uri {
267 (
268 crate::uri_path::parent(&path_str),
269 crate::uri_path::suffix(&path_str),
270 )
271 } else {
272 (
273 pp::parent(&path_str, fmt),
274 pp::extension(&path_str, fmt).to_string(),
275 )
276 };
277 if new_stem.is_empty() && !ext.is_empty() {
278 return Err(ExpressionError::new(format!(
280 "with_stem: '{path_str}' has a non-empty suffix"
281 )));
282 }
283 let new_filename = format!("{new_stem}{ext}");
284 if !is_valid_name(&new_filename, fmt) {
285 return Err(ExpressionError::new(format!(
286 "with_stem: Invalid name '{new_filename}'"
287 )));
288 }
289 let result = if is_uri {
290 format!("{parent}/{new_filename}")
291 } else {
292 join_parent_and_name(&parent, &new_filename, fmt)
293 };
294 Ok(ExprValue::new_path(result, fmt))
295}
296
297pub fn with_suffix_fn(ctx: Ctx, a: &[ExprValue]) -> R {
298 let (path_str, fmt) = get_path(&a[0], ctx)?;
299 let new_suffix = get_str_arg(a, 1);
300 if !is_valid_suffix(&new_suffix, fmt) {
301 return Err(ExpressionError::new(format!(
302 "with_suffix: Invalid suffix '{new_suffix}'"
303 )));
304 }
305 ctx.count_string_ops(path_str.len())?;
306 if has_empty_name(&path_str, fmt) {
307 return Err(ExpressionError::new(format!(
308 "with_suffix: '{path_str}' has an empty name"
309 )));
310 }
311 let is_uri = crate::uri_path::is_uri(&path_str);
312 let (parent, stem) = if is_uri {
313 (
314 crate::uri_path::parent(&path_str),
315 crate::uri_path::stem(&path_str),
316 )
317 } else {
318 (
319 pp::parent(&path_str, fmt),
320 pp::file_stem(&path_str, fmt).to_string(),
321 )
322 };
323 let new_filename = format!("{stem}{new_suffix}");
324 if !is_valid_name(&new_filename, fmt) {
331 return Err(ExpressionError::new(format!(
332 "with_suffix: Invalid name '{new_filename}'"
333 )));
334 }
335 let result = if is_uri {
336 format!("{parent}/{new_filename}")
337 } else {
338 join_parent_and_name(&parent, &new_filename, fmt)
339 };
340 Ok(ExprValue::new_path(result, fmt))
341}
342
343fn split_name_at_suffix(filename: &str) -> (&str, &str) {
349 match filename.rfind('.') {
350 Some(i) if i > 0 && i + 1 < filename.len() => (&filename[..i], &filename[i..]),
351 _ => (filename, ""),
352 }
353}
354
355pub fn with_number_fn(ctx: Ctx, a: &[ExprValue]) -> R {
356 let (path_str, fmt) = get_path(&a[0], ctx)?;
357 let num = match &a[1] {
358 ExprValue::Int(n) => *n,
359 _ => return Err(ExpressionError::new("with_number() requires int argument")),
360 };
361 ctx.count_string_ops(path_str.len())?;
362 if has_empty_name(&path_str, fmt) {
363 return Err(ExpressionError::new(format!(
364 "with_number: '{path_str}' has an empty name"
365 )));
366 }
367 let is_string = matches!(&a[0], ExprValue::String(_));
368 let result = if crate::uri_path::is_uri(&path_str) {
374 let parent = crate::uri_path::parent(&path_str);
375 let filename = crate::uri_path::name(&path_str);
376 let (stem, suffix) = split_name_at_suffix(&filename);
377 let new_stem = with_number_replace(stem, num)?;
378 format!("{parent}/{new_stem}{suffix}")
380 } else {
381 let (dir_part, filename) = pp::split(&path_str, fmt);
382 let (stem, suffix) = split_name_at_suffix(filename);
383 let new_stem = with_number_replace(stem, num)?;
384 let new_filename = format!("{new_stem}{suffix}");
385 join_parent_and_name(dir_part, &new_filename, fmt)
386 };
387 if is_string {
388 Ok(ExprValue::String(result))
389 } else {
390 Ok(ExprValue::new_path(result, fmt))
391 }
392}
393
394pub fn is_absolute_fn(ctx: Ctx, a: &[ExprValue]) -> R {
395 let (path_str, fmt) = get_path(&a[0], ctx)?;
396 Ok(ExprValue::Bool(is_absolute(&path_str, fmt)))
397}
398
399pub fn is_absolute(path_str: &str, fmt: PathFormat) -> bool {
401 if crate::uri_path::is_uri(path_str) {
402 return true;
403 }
404 let bytes = path_str.as_bytes();
405 if bytes.len() >= 2
407 && ((bytes[0] == b'/' && bytes[1] == b'/') || (bytes[0] == b'\\' && bytes[1] == b'\\'))
408 {
409 return true;
410 }
411 match fmt {
412 PathFormat::Windows => {
413 bytes.len() >= 3
414 && bytes[0].is_ascii_alphabetic()
415 && bytes[1] == b':'
416 && (bytes[2] == b'\\' || bytes[2] == b'/')
417 }
418 PathFormat::Posix | PathFormat::Uri => bytes.first() == Some(&b'/'),
419 }
420}
421
422pub fn join(left: &str, right: &str, fmt: PathFormat) -> String {
429 if is_absolute(right, fmt) {
430 return right.to_string();
431 }
432 if fmt == PathFormat::Windows {
437 let rb = right.as_bytes();
438 if rb.first() == Some(&b'/') || rb.first() == Some(&b'\\') {
439 let lb = left.as_bytes();
440 if lb.len() >= 2 && lb[0].is_ascii_alphabetic() && lb[1] == b':' {
442 return format!("{}{right}", &left[..2]);
443 }
444 if let Some(unc_root) = extract_unc_root(left) {
446 return format!("{unc_root}{right}");
447 }
448 }
449 }
450 let left_is_uri = crate::uri_path::is_uri(left);
451 let (sep, trim_chars): (&str, &[char]) = if left_is_uri {
452 ("/", &['/'])
453 } else {
454 match fmt {
455 PathFormat::Windows => ("\\", &['/', '\\']),
457 PathFormat::Posix | PathFormat::Uri => ("/", &['/']),
459 }
460 };
461 let left = left.trim_end_matches(trim_chars);
462 let right = if left_is_uri && fmt == PathFormat::Windows {
465 std::borrow::Cow::Owned(right.replace('\\', "/"))
466 } else {
467 std::borrow::Cow::Borrowed(right)
468 };
469 format!("{left}{sep}{right}")
470}
471
472pub fn non_uri_join(left: &str, right: &str, fmt: PathFormat) -> String {
479 if fmt == PathFormat::Windows {
481 let rb = right.as_bytes();
482 if rb.first() == Some(&b'/') || rb.first() == Some(&b'\\') {
483 let lb = left.as_bytes();
484 if lb.len() >= 2 && lb[0].is_ascii_alphabetic() && lb[1] == b':' {
485 return format!("{}{right}", &left[..2]);
486 }
487 if let Some(unc_root) = extract_unc_root(left) {
488 return format!("{unc_root}{right}");
489 }
490 }
491 }
492 let (sep, trim_chars): (&str, &[char]) = match fmt {
493 PathFormat::Windows => ("\\", &['/', '\\']),
494 PathFormat::Posix | PathFormat::Uri => ("/", &['/']),
495 };
496 let left = left.trim_end_matches(trim_chars);
497 format!("{left}{sep}{right}")
498}
499
500fn extract_unc_root(path: &str) -> Option<&str> {
503 let bytes = path.as_bytes();
504 if bytes.len() < 2 {
505 return None;
506 }
507 let prefix_char = bytes[0];
508 if !((prefix_char == b'\\' && bytes[1] == b'\\') || (prefix_char == b'/' && bytes[1] == b'/')) {
509 return None;
510 }
511 let rest = &path[2..];
513 let sep_after_server = rest.find(['/', '\\'])?;
514 let after_server = sep_after_server + 3; let share_start = after_server;
517 let sep_after_share = path[share_start..]
518 .find(['/', '\\'])
519 .map(|i| share_start + i)
520 .unwrap_or(path.len());
521 Some(&path[..sep_after_share])
522}
523
524fn path_starts_with(path: &str, base: &str, fmt: PathFormat) -> bool {
525 if fmt == PathFormat::Windows {
526 path.len() >= base.len() && path[..base.len()].eq_ignore_ascii_case(base)
527 } else {
528 path.starts_with(base)
529 }
530}
531
532pub fn is_relative_to_fn(ctx: Ctx, a: &[ExprValue]) -> R {
533 let (path_str, fmt) = get_path(&a[0], ctx)?;
534 let base = get_str_arg(a, 1);
535 ctx.count_string_ops(path_str.len().max(base.len()))?;
541 let is_rel = path_starts_with(&path_str, &base, fmt)
542 && (path_str.len() == base.len()
543 || base.ends_with('/')
544 || base.ends_with('\\')
545 || matches!(path_str.as_bytes().get(base.len()), Some(b'/' | b'\\')));
546 Ok(ExprValue::Bool(is_rel))
547}
548
549pub fn relative_to_fn(ctx: Ctx, a: &[ExprValue]) -> R {
550 let (path_str, fmt) = get_path(&a[0], ctx)?;
551 let base = get_str_arg(a, 1);
552 ctx.count_string_ops(path_str.len().max(base.len()))?;
557 let is_rel = path_starts_with(&path_str, &base, fmt)
558 && (path_str.len() == base.len()
559 || base.ends_with('/')
560 || base.ends_with('\\')
561 || matches!(path_str.as_bytes().get(base.len()), Some(b'/' | b'\\')));
562 if !is_rel {
563 return Err(ExpressionError::new(format!(
564 "relative_to failed: '{path_str}' is not relative to '{base}'"
565 )));
566 }
567 let rel = path_str[base.len()..]
568 .trim_start_matches('/')
569 .trim_start_matches('\\');
570 Ok(ExprValue::new_path(
571 if rel.is_empty() {
572 ".".to_string()
573 } else {
574 rel.to_string()
575 },
576 fmt,
577 ))
578}
579
580pub fn make_apply_path_mapping_fn(
588 rules: std::sync::Arc<Vec<crate::path_mapping::PathMappingRule>>,
589) -> impl Fn(&mut dyn EvalContext, &[ExprValue]) -> R + Send + Sync + 'static {
590 move |ctx, a| {
591 let (path_str, fmt) = get_path(&a[0], ctx)?;
592 ctx.count_string_ops(path_str.len())?;
599 let mapped =
600 crate::path_mapping::apply_rules_with_format(&rules, &path_str, ctx.path_format());
601 if mapped == path_str {
602 Ok(ExprValue::new_path(path_str, fmt))
603 } else {
604 Ok(ExprValue::new_path(mapped, fmt))
605 }
606 }
607}
608
609fn format_padded(num: i64, width: usize) -> String {
610 if num < 0 {
611 format!("-{:0>width$}", -num, width = width.saturating_sub(1))
612 } else {
613 format!("{:0>width$}", num, width = width)
614 }
615}
616
617const MAX_PADDING_WIDTH: usize = 32;
618
619fn with_number_replace(stem: &str, num: i64) -> Result<String, ExpressionError> {
620 if let Some(pct) = stem.rfind('%') {
622 let after = &stem[pct + 1..];
623 if after == "d" {
624 return Ok(format!("{}{}", &stem[..pct], num));
625 }
626 if after.starts_with('0') && after.ends_with('d') {
627 let width: usize = after[1..after.len() - 1].parse().unwrap_or(1);
628 if width > MAX_PADDING_WIDTH {
629 return Err(ExpressionError::new(format!(
630 "with_number: padding width {width} exceeds maximum of {MAX_PADDING_WIDTH}"
631 )));
632 }
633 return Ok(format!("{}{}", &stem[..pct], format_padded(num, width)));
634 }
635 }
636 if let Some(start) = stem.rfind('#') {
638 let hash_start = stem[..=start]
639 .rfind(|c: char| c != '#')
640 .map(|i| i + 1)
641 .unwrap_or(0);
642 let width = start - hash_start + 1;
643 if width > MAX_PADDING_WIDTH {
644 return Err(ExpressionError::new(format!(
645 "with_number: padding width {width} exceeds maximum of {MAX_PADDING_WIDTH}"
646 )));
647 }
648 return Ok(format!(
649 "{}{}",
650 &stem[..hash_start],
651 format_padded(num, width)
652 ));
653 }
654 let digit_start = stem.len()
656 - stem
657 .chars()
658 .rev()
659 .take_while(|c| c.is_ascii_digit())
660 .count();
661 if digit_start < stem.len() {
662 let width = stem.len() - digit_start;
663 return Ok(format!(
664 "{}{}",
665 &stem[..digit_start],
666 format_padded(num, width)
667 ));
668 }
669 Ok(format!("{}_{}", stem, format_padded(num, 4)))
671}
672
673pub fn prop_name(ctx: Ctx, a: &[ExprValue]) -> R {
676 let (path_str, fmt) = get_path(&a[0], ctx)?;
677 ctx.count_string_ops(path_str.len())?;
678 if crate::uri_path::is_uri(&path_str) {
679 return Ok(ExprValue::String(crate::uri_path::name(&path_str)));
680 }
681 Ok(ExprValue::String(pp::file_name(&path_str, fmt).to_string()))
682}
683
684pub fn prop_stem(ctx: Ctx, a: &[ExprValue]) -> R {
685 let (path_str, fmt) = get_path(&a[0], ctx)?;
686 ctx.count_string_ops(path_str.len())?;
687 if crate::uri_path::is_uri(&path_str) {
688 return Ok(ExprValue::String(crate::uri_path::stem(&path_str)));
689 }
690 Ok(ExprValue::String(pp::file_stem(&path_str, fmt).to_string()))
691}
692
693pub fn prop_suffix(ctx: Ctx, a: &[ExprValue]) -> R {
694 let (path_str, fmt) = get_path(&a[0], ctx)?;
695 ctx.count_string_ops(path_str.len())?;
696 Ok(ExprValue::String(if crate::uri_path::is_uri(&path_str) {
697 crate::uri_path::suffix(&path_str)
698 } else {
699 pp::extension(&path_str, fmt).to_string()
700 }))
701}
702
703pub fn prop_suffixes(ctx: Ctx, a: &[ExprValue]) -> R {
704 let (path_str, fmt) = get_path(&a[0], ctx)?;
705 ctx.count_string_ops(path_str.len())?;
706 if crate::uri_path::is_uri(&path_str) {
707 let suffixes: Vec<ExprValue> = crate::uri_path::suffixes(&path_str)
708 .into_iter()
709 .map(ExprValue::String)
710 .collect();
711 return ExprValue::make_list_checked(ctx, suffixes, crate::types::ExprType::STRING);
712 }
713 let suffixes: Vec<ExprValue> = pp::suffixes(&path_str, fmt)
714 .into_iter()
715 .map(ExprValue::String)
716 .collect();
717 ExprValue::make_list_checked(ctx, suffixes, crate::types::ExprType::STRING)
718}
719
720pub fn prop_parent(ctx: Ctx, a: &[ExprValue]) -> R {
721 let (path_str, fmt) = get_path(&a[0], ctx)?;
722 ctx.count_string_ops(path_str.len())?;
723 if crate::uri_path::is_uri(&path_str) {
724 return Ok(ExprValue::new_path(crate::uri_path::parent(&path_str), fmt));
725 }
726 Ok(ExprValue::new_path(pp::parent(&path_str, fmt), fmt))
727}
728
729pub fn prop_parts(ctx: Ctx, a: &[ExprValue]) -> R {
730 let (path_str, fmt) = get_path(&a[0], ctx)?;
731 ctx.count_string_ops(path_str.len())?;
732 if crate::uri_path::is_uri(&path_str) {
733 let parts: Vec<ExprValue> = crate::uri_path::parts(&path_str)
734 .into_iter()
735 .map(ExprValue::String)
736 .collect();
737 return ExprValue::make_list_checked(ctx, parts, crate::types::ExprType::STRING);
738 }
739 let parts: Vec<ExprValue> = pp::parts(&path_str, fmt)
740 .into_iter()
741 .map(ExprValue::String)
742 .collect();
743 ExprValue::make_list_checked(ctx, parts, crate::types::ExprType::STRING)
744}