Skip to main content

agent_playground/utils/
symlink.rs

1use 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)]
10/// A directory mounted into the temporary playground as a symbolic link.
11pub struct DirectoryMount {
12    /// Absolute source directory on the host filesystem.
13    pub source: PathBuf,
14    /// Relative destination path inside the temporary playground.
15    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}