upstream_rs/services/integration/
symlink_manager.rs1use anyhow::{Context, Result};
2use std::fs;
3use std::path::Path;
4
5#[cfg(windows)]
6use std::ffi::OsStr;
7
8pub struct SymlinkManager<'a> {
9 symlinks_dir: &'a Path,
10}
11
12impl<'a> SymlinkManager<'a> {
13 fn remove_link_path(path: &Path, context_message: &'static str) -> Result<()> {
14 match fs::symlink_metadata(path) {
15 Ok(metadata) => {
16 if metadata.is_dir() && !metadata.file_type().is_symlink() {
17 anyhow::bail!(
18 "Refusing to remove directory at '{}' while managing symlink",
19 path.display()
20 );
21 }
22 fs::remove_file(path).context(context_message)?;
23 }
24 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
25 Err(err) => return Err(err).context(context_message),
26 }
27
28 Ok(())
29 }
30
31 fn platform_link_path(link: &Path) -> std::path::PathBuf {
32 #[cfg(windows)]
33 {
34 if link.extension() != Some(OsStr::new("exe")) {
35 return link.with_extension("exe");
36 }
37 }
38
39 link.to_path_buf()
40 }
41
42 pub fn new(symlinks_dir: &'a Path) -> Self {
43 Self { symlinks_dir }
44 }
45
46 pub fn add_link(&self, exec_path: &Path, name: &str) -> Result<()> {
48 if !exec_path.exists() {
49 anyhow::bail!("Target file not found: {}", exec_path.display());
50 }
51
52 let base_link = self.symlinks_dir.join(name);
53 let symlink = Self::platform_link_path(&base_link);
54
55 Self::remove_link_path(&symlink, "Failed to remove existing symlink")?;
57 if base_link != symlink {
59 Self::remove_link_path(&base_link, "Failed to remove stale symlink")?;
60 }
61
62 Self::create_symlink(exec_path, &symlink)?;
63 Ok(())
64 }
65
66 pub fn remove_link(&self, name: &str) -> Result<()> {
68 let base_link = self.symlinks_dir.join(name);
69 let symlink = Self::platform_link_path(&base_link);
70
71 Self::remove_link_path(&symlink, "Failed to remove symlink")?;
72 if base_link != symlink {
73 Self::remove_link_path(&base_link, "Failed to remove stale symlink")?;
74 }
75
76 Ok(())
77 }
78
79 #[cfg(unix)]
80 fn create_symlink(target_path: &Path, symlink: &Path) -> Result<()> {
81 std::os::unix::fs::symlink(target_path, symlink).context("Failed to create symlink")
82 }
83
84 #[cfg(windows)]
85 fn create_symlink(target_path: &Path, link: &Path) -> Result<()> {
86 fs::hard_link(target_path, link).context("Failed to create hardlink")
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 #[cfg(unix)]
93 use super::SymlinkManager;
94 #[cfg(unix)]
95 use std::path::{Path, PathBuf};
96 #[cfg(unix)]
97 use std::time::{SystemTime, UNIX_EPOCH};
98 #[cfg(unix)]
99 use std::{fs, io};
100
101 #[cfg(unix)]
102 fn temp_root(name: &str) -> PathBuf {
103 let nanos = SystemTime::now()
104 .duration_since(UNIX_EPOCH)
105 .map(|d| d.as_nanos())
106 .unwrap_or(0);
107 std::env::temp_dir().join(format!("upstream-symlink-test-{name}-{nanos}"))
108 }
109
110 #[cfg(unix)]
111 fn cleanup(path: &Path) -> io::Result<()> {
112 fs::remove_dir_all(path)
113 }
114
115 #[cfg(unix)]
116 #[test]
117 fn add_link_replaces_dangling_symlink() {
118 let root = temp_root("replace-dangling");
119 let symlinks_dir = root.join("symlinks");
120 let missing_target = root.join("missing-target");
121 let new_target = root.join("new-target");
122 let link_name = "arduino";
123 let link_path = symlinks_dir.join(link_name);
124
125 fs::create_dir_all(&symlinks_dir).expect("create symlink dir");
126 fs::write(&new_target, b"new-target").expect("write new target");
127 std::os::unix::fs::symlink(&missing_target, &link_path).expect("create dangling symlink");
128 assert!(
129 !link_path.exists(),
130 "dangling symlink should not exist via exists()"
131 );
132 assert!(
133 fs::symlink_metadata(&link_path).is_ok(),
134 "dangling symlink should still be present on disk"
135 );
136
137 let manager = SymlinkManager::new(&symlinks_dir);
138 manager
139 .add_link(&new_target, link_name)
140 .expect("replace dangling symlink");
141
142 let target = fs::read_link(&link_path).expect("read link target");
143 assert_eq!(target, new_target);
144
145 cleanup(&root).expect("cleanup");
146 }
147
148 #[cfg(unix)]
149 #[test]
150 fn remove_link_removes_dangling_symlink() {
151 let root = temp_root("remove-dangling");
152 let symlinks_dir = root.join("symlinks");
153 let missing_target = root.join("missing-target");
154 let link_name = "arduino";
155 let link_path = symlinks_dir.join(link_name);
156
157 fs::create_dir_all(&symlinks_dir).expect("create symlink dir");
158 std::os::unix::fs::symlink(&missing_target, &link_path).expect("create dangling symlink");
159 assert!(
160 fs::symlink_metadata(&link_path).is_ok(),
161 "dangling symlink should be present before removal"
162 );
163
164 let manager = SymlinkManager::new(&symlinks_dir);
165 manager
166 .remove_link(link_name)
167 .expect("remove dangling symlink");
168
169 assert!(
170 fs::symlink_metadata(&link_path).is_err(),
171 "dangling symlink should be removed"
172 );
173
174 cleanup(&root).expect("cleanup");
175 }
176}