agent_playground/utils/
symlink.rs1use std::{
2 fs, io,
3 path::{Path, PathBuf},
4 str::FromStr,
5};
6
7use anyhow::{Context, Result, bail};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct DirectoryMount {
12 pub source: PathBuf,
14 pub destination: PathBuf,
16}
17
18impl FromStr for DirectoryMount {
19 type Err = String;
20
21 fn from_str(raw: &str) -> std::result::Result<Self, Self::Err> {
22 parse_directory_mount(raw).map_err(|error| error.to_string())
23 }
24}
25
26pub(crate) fn parse_directory_mount(raw: &str) -> Result<DirectoryMount> {
27 if let Ok(source) = resolve_directory_mount_source(raw) {
28 return Ok(DirectoryMount {
29 destination: default_directory_mount_destination(&source)?,
30 source,
31 });
32 }
33
34 let (source_raw, destination_raw) = raw
35 .rsplit_once(':')
36 .context("mount spec must be SOURCE or SOURCE:RELATIVE_DESTINATION")?;
37 let source = resolve_directory_mount_source(source_raw)?;
38 let destination = parse_directory_mount_destination(destination_raw)?;
39
40 Ok(DirectoryMount {
41 source,
42 destination,
43 })
44}
45
46pub(crate) fn apply_directory_mounts(working_dir: &Path, mounts: &[DirectoryMount]) -> Result<()> {
47 for mount in mounts {
48 let destination = working_dir.join(&mount.destination);
49 ensure_destination_absent(&destination)?;
50
51 if let Some(parent) = destination.parent() {
52 fs::create_dir_all(parent)
53 .with_context(|| format!("failed to create {}", parent.display()))?;
54 }
55
56 create_symlink(&mount.source, &destination, true).with_context(|| {
57 format!(
58 "failed to mount {} at {}",
59 mount.source.display(),
60 destination.display()
61 )
62 })?;
63 }
64
65 Ok(())
66}
67
68pub(crate) fn copy_symlink(source: &Path, destination: &Path) -> Result<()> {
69 if let Some(parent) = destination.parent() {
70 fs::create_dir_all(parent)
71 .with_context(|| format!("failed to create {}", parent.display()))?;
72 }
73
74 let link_target = fs::read_link(source)
75 .with_context(|| format!("failed to read symlink {}", source.display()))?;
76
77 #[cfg(unix)]
78 let is_dir_target = false;
79
80 #[cfg(windows)]
81 let is_dir_target = fs::metadata(source)
82 .map(|metadata| metadata.is_dir())
83 .unwrap_or(false);
84
85 create_symlink(&link_target, destination, is_dir_target).with_context(|| {
86 format!(
87 "failed to recreate symlink {} -> {}",
88 destination.display(),
89 link_target.display()
90 )
91 })?;
92
93 Ok(())
94}
95
96fn resolve_directory_mount_source(raw: &str) -> Result<PathBuf> {
97 let source = fs::canonicalize(raw)
98 .with_context(|| format!("failed to resolve mount source directory '{}'", raw))?;
99 let metadata = fs::metadata(&source)
100 .with_context(|| format!("failed to inspect mount source {}", source.display()))?;
101
102 if !metadata.is_dir() {
103 bail!("mount source '{}' is not a directory", raw);
104 }
105
106 Ok(source)
107}
108
109fn default_directory_mount_destination(source: &Path) -> Result<PathBuf> {
110 source
111 .file_name()
112 .map(PathBuf::from)
113 .context("mount source must have a directory name or an explicit destination")
114}
115
116fn parse_directory_mount_destination(raw: &str) -> Result<PathBuf> {
117 if raw.is_empty() {
118 bail!("mount destination cannot be empty");
119 }
120
121 let destination = PathBuf::from(raw);
122 if destination.is_absolute() {
123 bail!(
124 "mount destination '{}' must be a relative path inside the playground",
125 raw
126 );
127 }
128
129 for component in destination.components() {
130 if !matches!(component, std::path::Component::Normal(_)) {
131 bail!(
132 "mount destination '{}' must only contain normal path segments",
133 raw
134 );
135 }
136 }
137
138 Ok(destination)
139}
140
141fn ensure_destination_absent(destination: &Path) -> Result<()> {
142 if fs::symlink_metadata(destination).is_ok() {
143 bail!(
144 "mount destination already exists inside playground: {}",
145 destination.display()
146 );
147 }
148
149 Ok(())
150}
151
152#[cfg(unix)]
153fn create_symlink(source: &Path, destination: &Path, _is_dir: bool) -> io::Result<()> {
154 std::os::unix::fs::symlink(source, destination)
155}
156
157#[cfg(windows)]
158fn create_symlink(source: &Path, destination: &Path, is_dir: bool) -> io::Result<()> {
159 if is_dir {
160 std::os::windows::fs::symlink_dir(source, destination)
161 } else {
162 std::os::windows::fs::symlink_file(source, destination)
163 }
164}