1use std::borrow::Cow;
2use std::fmt::Display;
3use std::path::{Path, PathBuf};
4
5use tempfile::NamedTempFile;
6use tracing::{debug, error, info, trace, warn};
7
8pub use crate::path::*;
9
10pub mod cachedir;
11mod path;
12pub mod which;
13
14pub fn with_added_extension<'a>(path: &'a Path, extension: &str) -> Cow<'a, Path> {
22 let Some(name) = path.file_name() else {
23 return Cow::Borrowed(path);
25 };
26 let mut name = name.to_os_string();
27 name.push(".");
28 name.push(extension.trim_start_matches('.'));
29 Cow::Owned(path.with_file_name(name))
30}
31
32pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
36 if left == right {
38 return Some(true);
39 }
40
41 if let Ok(value) = same_file::is_same_file(left, right) {
43 return Some(value);
44 }
45
46 if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = (
48 left.parent(),
49 right.parent(),
50 left.file_name(),
51 right.file_name(),
52 ) {
53 match same_file::is_same_file(left_parent, right_parent) {
54 Ok(true) => return Some(left_name == right_name),
55 Ok(false) => return Some(false),
56 _ => (),
57 }
58 }
59
60 None
62}
63
64#[cfg(feature = "tokio")]
74pub async fn read_to_string_transcode(path: impl AsRef<Path>) -> std::io::Result<String> {
75 use std::io::Read;
76
77 use encoding_rs_io::DecodeReaderBytes;
78
79 let path = path.as_ref();
80 let raw = if path == Path::new("-") {
81 let mut buf = Vec::with_capacity(1024);
82 std::io::stdin().read_to_end(&mut buf)?;
83 buf
84 } else {
85 fs_err::tokio::read(path).await?
86 };
87 let mut buf = String::with_capacity(1024);
88 DecodeReaderBytes::new(&*raw)
89 .read_to_string(&mut buf)
90 .map_err(|err| {
91 let path = path.display();
92 std::io::Error::other(format!("failed to decode file {path}: {err}"))
93 })?;
94 Ok(buf)
95}
96
97#[cfg(windows)]
107pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
108 if src.as_ref().is_file() {
110 return Err(std::io::Error::new(
111 std::io::ErrorKind::InvalidInput,
112 format!(
113 "Cannot create a junction for {}: is not a directory",
114 src.as_ref().display()
115 ),
116 ));
117 }
118
119 match junction::delete(dunce::simplified(dst.as_ref())) {
121 Ok(()) => match fs_err::remove_dir_all(dst.as_ref()) {
122 Ok(()) => {}
123 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
124 Err(err) => return Err(err),
125 },
126 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
127 Err(err) => return Err(err),
128 }
129
130 junction::create(
132 dunce::simplified(src.as_ref()),
133 dunce::simplified(dst.as_ref()),
134 )
135}
136
137#[cfg(unix)]
141pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
142 match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
144 Ok(()) => Ok(()),
145 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
146 let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?;
148 let temp_file = temp_dir.path().join("link");
149 fs_err::os::unix::fs::symlink(src, &temp_file)?;
150
151 fs_err::rename(&temp_file, dst.as_ref())?;
153
154 Ok(())
155 }
156 Err(err) => Err(err),
157 }
158}
159
160#[cfg(windows)]
168pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
169 if src.as_ref().is_file() {
171 return Err(std::io::Error::new(
172 std::io::ErrorKind::InvalidInput,
173 format!(
174 "Cannot create a junction for {}: is not a directory",
175 src.as_ref().display()
176 ),
177 ));
178 }
179
180 junction::create(
181 dunce::simplified(src.as_ref()),
182 dunce::simplified(dst.as_ref()),
183 )
184}
185
186#[cfg(unix)]
188pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
189 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
190}
191
192#[cfg(unix)]
193pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
194 fs_err::remove_file(path.as_ref())
195}
196
197pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
206 #[cfg(windows)]
207 {
208 fs_err::copy(src.as_ref(), dst.as_ref())?;
209 }
210 #[cfg(unix)]
211 {
212 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
213 }
214
215 Ok(())
216}
217
218#[cfg(windows)]
219pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
220 match junction::delete(dunce::simplified(path.as_ref())) {
221 Ok(()) => match fs_err::remove_dir_all(path.as_ref()) {
222 Ok(()) => Ok(()),
223 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
224 Err(err) => Err(err),
225 },
226 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
227 Err(err) => Err(err),
228 }
229}
230
231#[cfg(unix)]
236pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
237 use std::os::unix::fs::PermissionsExt;
238 tempfile::Builder::new()
239 .permissions(std::fs::Permissions::from_mode(0o666))
240 .tempfile_in(path)
241}
242
243#[cfg(not(unix))]
245pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
246 tempfile::Builder::new().tempfile_in(path)
247}
248
249#[cfg(feature = "tokio")]
251pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
252 let temp_file = tempfile_in(
253 path.as_ref()
254 .parent()
255 .expect("Write path must have a parent"),
256 )?;
257 fs_err::tokio::write(&temp_file, &data).await?;
258 persist_with_retry(temp_file, path.as_ref()).await
259}
260
261pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
263 let temp_file = tempfile_in(
264 path.as_ref()
265 .parent()
266 .expect("Write path must have a parent"),
267 )?;
268 fs_err::write(&temp_file, &data)?;
269 persist_with_retry_sync(temp_file, path.as_ref())
270}
271
272pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
274 let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
275 fs_err::copy(from.as_ref(), &temp_file)?;
276 persist_with_retry_sync(temp_file, to.as_ref())
277}
278
279#[cfg(windows)]
280fn backoff_file_move() -> backon::ExponentialBackoff {
281 use backon::BackoffBuilder;
282 backon::ExponentialBuilder::default()
287 .with_min_delay(std::time::Duration::from_millis(10))
288 .with_max_times(9)
289 .build()
290}
291
292#[cfg(feature = "tokio")]
294pub async fn rename_with_retry(
295 from: impl AsRef<Path>,
296 to: impl AsRef<Path>,
297) -> Result<(), std::io::Error> {
298 #[cfg(windows)]
299 {
300 use backon::Retryable;
301 let from = from.as_ref();
307 let to = to.as_ref();
308
309 let rename = async || fs_err::rename(from, to);
310
311 rename
312 .retry(backoff_file_move())
313 .sleep(tokio::time::sleep)
314 .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
315 .notify(|err, _dur| {
316 warn!(
317 "Retrying rename from {} to {} due to transient error: {}",
318 from.display(),
319 to.display(),
320 err
321 );
322 })
323 .await
324 }
325 #[cfg(not(windows))]
326 {
327 fs_err::tokio::rename(from, to).await
328 }
329}
330
331#[cfg_attr(not(windows), allow(unused_variables))]
334pub fn with_retry_sync(
335 from: impl AsRef<Path>,
336 to: impl AsRef<Path>,
337 operation_name: &str,
338 operation: impl Fn() -> Result<(), std::io::Error>,
339) -> Result<(), std::io::Error> {
340 #[cfg(windows)]
341 {
342 use backon::BlockingRetryable;
343 let from = from.as_ref();
349 let to = to.as_ref();
350
351 operation
352 .retry(backoff_file_move())
353 .sleep(std::thread::sleep)
354 .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
355 .notify(|err, _dur| {
356 warn!(
357 "Retrying {} from {} to {} due to transient error: {}",
358 operation_name,
359 from.display(),
360 to.display(),
361 err
362 );
363 })
364 .call()
365 .map_err(|err| {
366 std::io::Error::other(format!(
367 "Failed {} {} to {}: {}",
368 operation_name,
369 from.display(),
370 to.display(),
371 err
372 ))
373 })
374 }
375 #[cfg(not(windows))]
376 {
377 operation()
378 }
379}
380
381#[cfg(windows)]
383enum PersistRetryError {
384 Persist(String),
386 LostState,
388}
389
390pub async fn persist_with_retry(
392 from: NamedTempFile,
393 to: impl AsRef<Path>,
394) -> Result<(), std::io::Error> {
395 #[cfg(windows)]
396 {
397 use backon::Retryable;
398 let to = to.as_ref();
404
405 let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
425 let persist = || {
426 let from2 = from.clone();
428
429 async move {
430 let maybe_file: Option<NamedTempFile> = from2
431 .lock()
432 .map_err(|_| PersistRetryError::LostState)?
433 .take();
434 if let Some(file) = maybe_file {
435 file.persist(to).map_err(|err| {
436 let error_message: String = err.to_string();
437 if let Ok(mut guard) = from2.lock() {
439 *guard = Some(err.file);
440 PersistRetryError::Persist(error_message)
441 } else {
442 PersistRetryError::LostState
443 }
444 })
445 } else {
446 Err(PersistRetryError::LostState)
447 }
448 }
449 };
450
451 let persisted = persist
452 .retry(backoff_file_move())
453 .sleep(tokio::time::sleep)
454 .when(|err| matches!(err, PersistRetryError::Persist(_)))
455 .notify(|err, _dur| {
456 if let PersistRetryError::Persist(error_message) = err {
457 warn!(
458 "Retrying to persist temporary file to {}: {}",
459 to.display(),
460 error_message,
461 );
462 }
463 })
464 .await;
465
466 match persisted {
467 Ok(_) => Ok(()),
468 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
469 "Failed to persist temporary file to {}: {}",
470 to.display(),
471 error_message,
472 ))),
473 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
474 "Failed to retrieve temporary file while trying to persist to {}",
475 to.display()
476 ))),
477 }
478 }
479 #[cfg(not(windows))]
480 {
481 async { fs_err::rename(from, to) }.await
482 }
483}
484
485pub fn persist_with_retry_sync(
487 from: NamedTempFile,
488 to: impl AsRef<Path>,
489) -> Result<(), std::io::Error> {
490 #[cfg(windows)]
491 {
492 use backon::BlockingRetryable;
493 let to = to.as_ref();
499
500 let mut from = Some(from);
505 let persist = || {
506 if let Some(file) = from.take() {
508 file.persist(to).map_err(|err| {
509 let error_message = err.to_string();
510 from = Some(err.file);
512 PersistRetryError::Persist(error_message)
513 })
514 } else {
515 Err(PersistRetryError::LostState)
516 }
517 };
518
519 let persisted = persist
520 .retry(backoff_file_move())
521 .sleep(std::thread::sleep)
522 .when(|err| matches!(err, PersistRetryError::Persist(_)))
523 .notify(|err, _dur| {
524 if let PersistRetryError::Persist(error_message) = err {
525 warn!(
526 "Retrying to persist temporary file to {}: {}",
527 to.display(),
528 error_message,
529 );
530 }
531 })
532 .call();
533
534 match persisted {
535 Ok(_) => Ok(()),
536 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
537 "Failed to persist temporary file to {}: {}",
538 to.display(),
539 error_message,
540 ))),
541 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
542 "Failed to retrieve temporary file while trying to persist to {}",
543 to.display()
544 ))),
545 }
546 }
547 #[cfg(not(windows))]
548 {
549 fs_err::rename(from, to)
550 }
551}
552
553pub fn directories(
557 path: impl AsRef<Path>,
558) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
559 let entries = match path.as_ref().read_dir() {
560 Ok(entries) => Some(entries),
561 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
562 Err(err) => return Err(err),
563 };
564 Ok(entries
565 .into_iter()
566 .flatten()
567 .filter_map(|entry| match entry {
568 Ok(entry) => Some(entry),
569 Err(err) => {
570 warn!("Failed to read entry: {err}");
571 None
572 }
573 })
574 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
575 .map(|entry| entry.path()))
576}
577
578pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
582 let entries = match path.as_ref().read_dir() {
583 Ok(entries) => Some(entries),
584 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
585 Err(err) => return Err(err),
586 };
587 Ok(entries
588 .into_iter()
589 .flatten()
590 .filter_map(|entry| match entry {
591 Ok(entry) => Some(entry),
592 Err(err) => {
593 warn!("Failed to read entry: {err}");
594 None
595 }
596 })
597 .map(|entry| entry.path()))
598}
599
600pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
604 let entries = match path.as_ref().read_dir() {
605 Ok(entries) => Some(entries),
606 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
607 Err(err) => return Err(err),
608 };
609 Ok(entries
610 .into_iter()
611 .flatten()
612 .filter_map(|entry| match entry {
613 Ok(entry) => Some(entry),
614 Err(err) => {
615 warn!("Failed to read entry: {err}");
616 None
617 }
618 })
619 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
620 .map(|entry| entry.path()))
621}
622
623pub fn is_temporary(path: impl AsRef<Path>) -> bool {
625 path.as_ref()
626 .file_name()
627 .and_then(|name| name.to_str())
628 .is_some_and(|name| name.starts_with(".tmp"))
629}
630
631pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
638 executable
639 .as_ref()
640 .parent()
641 .and_then(Path::parent)
642 .is_some_and(is_virtualenv_base)
643}
644
645pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
652 path.as_ref().join("pyvenv.cfg").is_file()
653}
654
655fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
657 match err {
658 std::fs::TryLockError::WouldBlock => true,
659 std::fs::TryLockError::Error(err) => {
660 if cfg!(windows) && err.raw_os_error() == Some(33) {
662 return true;
663 }
664 false
665 }
666 }
667}
668
669#[derive(Debug)]
671#[must_use]
672pub struct LockedFile(fs_err::File);
673
674impl LockedFile {
675 fn lock_file_blocking(file: fs_err::File, resource: &str) -> Result<Self, std::io::Error> {
677 trace!(
678 "Checking lock for `{resource}` at `{}`",
679 file.path().user_display()
680 );
681 match file.file().try_lock() {
682 Ok(()) => {
683 debug!("Acquired lock for `{resource}`");
684 Ok(Self(file))
685 }
686 Err(err) => {
687 if !is_known_already_locked_error(&err) {
689 debug!("Try lock error: {err:?}");
690 }
691 info!(
692 "Waiting to acquire lock for `{resource}` at `{}`",
693 file.path().user_display(),
694 );
695 file.lock()?;
696 debug!("Acquired lock for `{resource}`");
697 Ok(Self(file))
698 }
699 }
700 }
701
702 fn lock_file_no_wait(file: fs_err::File, resource: &str) -> Option<Self> {
704 trace!(
705 "Checking lock for `{resource}` at `{}`",
706 file.path().user_display()
707 );
708 match file.try_lock() {
709 Ok(()) => {
710 debug!("Acquired lock for `{resource}`");
711 Some(Self(file))
712 }
713 Err(err) => {
714 if !is_known_already_locked_error(&err) {
716 debug!("Try lock error: {err:?}");
717 }
718 debug!("Lock is busy for `{resource}`");
719 None
720 }
721 }
722 }
723
724 fn lock_file_shared_blocking(
727 file: fs_err::File,
728 resource: &str,
729 ) -> Result<Self, std::io::Error> {
730 trace!(
731 "Checking shared lock for `{resource}` at `{}`",
732 file.path().user_display()
733 );
734 match file.try_lock_shared() {
735 Ok(()) => {
736 debug!("Acquired shared lock for `{resource}`");
737 Ok(Self(file))
738 }
739 Err(err) => {
740 if !is_known_already_locked_error(&err) {
742 debug!("Try lock error: {err:?}");
743 }
744 info!(
745 "Waiting to acquire shared lock for `{resource}` at `{}`",
746 file.path().user_display(),
747 );
748 file.lock_shared()?;
749 debug!("Acquired shared lock for `{resource}`");
750 Ok(Self(file))
751 }
752 }
753 }
754
755 pub fn acquire_blocking(
760 path: impl AsRef<Path>,
761 resource: impl Display,
762 ) -> Result<Self, std::io::Error> {
763 let file = Self::create(path)?;
764 let resource = resource.to_string();
765 Self::lock_file_blocking(file, &resource)
766 }
767
768 pub fn acquire_shared_blocking(
773 path: impl AsRef<Path>,
774 resource: impl Display,
775 ) -> Result<Self, std::io::Error> {
776 let file = Self::create(path)?;
777 let resource = resource.to_string();
778 Self::lock_file_shared_blocking(file, &resource)
779 }
780
781 #[cfg(feature = "tokio")]
783 pub async fn acquire(
784 path: impl AsRef<Path>,
785 resource: impl Display,
786 ) -> Result<Self, std::io::Error> {
787 let file = Self::create(path)?;
788 let resource = resource.to_string();
789 tokio::task::spawn_blocking(move || Self::lock_file_blocking(file, &resource)).await?
790 }
791
792 #[cfg(feature = "tokio")]
794 pub async fn acquire_shared(
795 path: impl AsRef<Path>,
796 resource: impl Display,
797 ) -> Result<Self, std::io::Error> {
798 let file = Self::create(path)?;
799 let resource = resource.to_string();
800 tokio::task::spawn_blocking(move || Self::lock_file_shared_blocking(file, &resource))
801 .await?
802 }
803
804 pub fn acquire_no_wait(path: impl AsRef<Path>, resource: impl Display) -> Option<Self> {
810 let file = Self::create(path).ok()?;
811 let resource = resource.to_string();
812 Self::lock_file_no_wait(file, &resource)
813 }
814
815 #[cfg(unix)]
816 fn create(path: impl AsRef<Path>) -> Result<fs_err::File, std::io::Error> {
817 use std::os::unix::fs::PermissionsExt;
818
819 if let Ok(file) = fs_err::OpenOptions::new()
821 .read(true)
822 .write(true)
823 .open(path.as_ref())
824 {
825 return Ok(file);
826 }
827
828 let file = if let Some(parent) = path.as_ref().parent() {
831 NamedTempFile::new_in(parent)?
832 } else {
833 NamedTempFile::new()?
834 };
835 if let Err(err) = file
836 .as_file()
837 .set_permissions(std::fs::Permissions::from_mode(0o777))
838 {
839 warn!("Failed to set permissions on temporary file: {err}");
840 }
841
842 match file.persist_noclobber(path.as_ref()) {
844 Ok(file) => Ok(fs_err::File::from_parts(file, path.as_ref())),
845 Err(err) => {
846 if err.error.kind() == std::io::ErrorKind::AlreadyExists {
847 fs_err::OpenOptions::new()
848 .read(true)
849 .write(true)
850 .open(path.as_ref())
851 } else {
852 Err(err.error)
853 }
854 }
855 }
856 }
857
858 #[cfg(not(unix))]
859 fn create(path: impl AsRef<Path>) -> std::io::Result<fs_err::File> {
860 fs_err::OpenOptions::new()
861 .read(true)
862 .write(true)
863 .create(true)
864 .open(path.as_ref())
865 }
866}
867
868impl Drop for LockedFile {
869 fn drop(&mut self) {
870 if let Err(err) = self.0.unlock() {
871 error!(
872 "Failed to unlock resource at `{}`; program may be stuck: {err}",
873 self.0.path().display()
874 );
875 } else {
876 debug!("Released lock at `{}`", self.0.path().display());
877 }
878 }
879}
880
881#[cfg(feature = "tokio")]
883pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
884 reader: Reader,
885 callback: Callback,
886}
887
888#[cfg(feature = "tokio")]
889impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
890 ProgressReader<Reader, Callback>
891{
892 pub fn new(reader: Reader, callback: Callback) -> Self {
894 Self { reader, callback }
895 }
896}
897
898#[cfg(feature = "tokio")]
899impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
900 for ProgressReader<Reader, Callback>
901{
902 fn poll_read(
903 mut self: std::pin::Pin<&mut Self>,
904 cx: &mut std::task::Context<'_>,
905 buf: &mut tokio::io::ReadBuf<'_>,
906 ) -> std::task::Poll<std::io::Result<()>> {
907 std::pin::Pin::new(&mut self.as_mut().reader)
908 .poll_read(cx, buf)
909 .map_ok(|()| {
910 (self.callback)(buf.filled().len());
911 })
912 }
913}
914
915pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
917 fs_err::create_dir_all(&dst)?;
918 for entry in fs_err::read_dir(src.as_ref())? {
919 let entry = entry?;
920 let ty = entry.file_type()?;
921 if ty.is_dir() {
922 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
923 } else {
924 fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
925 }
926 }
927 Ok(())
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933 use std::path::PathBuf;
934
935 #[test]
936 fn test_with_added_extension() {
937 let path = PathBuf::from("python");
939 let result = with_added_extension(&path, "exe");
940 assert_eq!(result, PathBuf::from("python.exe"));
941
942 let path = PathBuf::from("awslabs.cdk-mcp-server");
944 let result = with_added_extension(&path, "exe");
945 assert_eq!(result, PathBuf::from("awslabs.cdk-mcp-server.exe"));
946
947 let path = PathBuf::from("org.example.tool");
949 let result = with_added_extension(&path, "exe");
950 assert_eq!(result, PathBuf::from("org.example.tool.exe"));
951
952 let path = PathBuf::from("script");
954 let result = with_added_extension(&path, "ps1");
955 assert_eq!(result, PathBuf::from("script.ps1"));
956
957 let path = PathBuf::from("some/path/to/awslabs.cdk-mcp-server");
959 let result = with_added_extension(&path, "exe");
960 assert_eq!(
961 result,
962 PathBuf::from("some/path/to/awslabs.cdk-mcp-server.exe")
963 );
964
965 let path = PathBuf::new();
967 let result = with_added_extension(&path, "exe");
968 assert_eq!(result, path); }
970}