strict_path/path/virtual_path/links.rs
1//! Symlink and junction operations for `VirtualPath`.
2//!
3//! All link operations delegate to the inner `StrictPath`, which enforces boundary checks.
4//! The virtual-path view is preserved across link creation.
5use super::VirtualPath;
6use std::path::Path;
7
8impl<Marker> VirtualPath<Marker> {
9 /// Create a symlink at `link_path` pointing to this virtual path (same virtual root required).
10 ///
11 /// Both `self` (target) and `link_path` must be `VirtualPath` instances created via `virtual_join()`,
12 /// which ensures all paths are clamped to the virtual root. Absolute paths like `"/etc/config"`
13 /// passed to `virtual_join()` are automatically clamped to `vroot/etc/config`, ensuring symlinks
14 /// cannot escape the virtual root boundary.
15 ///
16 /// # Examples
17 ///
18 /// ```rust
19 /// # use strict_path::VirtualRoot;
20 /// # let td = tempfile::tempdir().unwrap();
21 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
22 ///
23 /// // Create target file
24 /// let target = vroot.virtual_join("/etc/config/app.conf")?;
25 /// target.create_parent_dir_all()?;
26 /// target.write(b"config data")?;
27 ///
28 /// // Ensure link parent directory exists (Windows requires this for symlink creation)
29 /// let link = vroot.virtual_join("/links/config.link")?;
30 /// link.create_parent_dir_all()?;
31 ///
32 /// // Create symlink - may fail on Windows without Developer Mode/admin privileges
33 /// if let Err(e) = target.virtual_symlink("/links/config.link") {
34 /// // Skip test if we don't have symlink privileges (Windows ERROR_PRIVILEGE_NOT_HELD = 1314)
35 /// #[cfg(windows)]
36 /// if e.raw_os_error() == Some(1314) { return Ok(()); }
37 /// return Err(e.into());
38 /// }
39 ///
40 /// assert_eq!(link.read_to_string()?, "config data");
41 /// # Ok::<(), Box<dyn std::error::Error>>(())
42 /// ```
43 pub fn virtual_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
44 let link_ref = link_path.as_ref();
45 let validated_link = if link_ref.is_absolute() {
46 match self.virtual_join(link_ref) {
47 Ok(p) => p,
48 Err(e) => return Err(std::io::Error::other(e)),
49 }
50 } else {
51 // Resolve as sibling
52 let parent = match self.virtualpath_parent() {
53 Ok(Some(p)) => p,
54 Ok(None) => match self
55 .inner
56 .boundary()
57 .clone()
58 .virtualize()
59 .into_virtualpath()
60 {
61 Ok(root) => root,
62 Err(e) => return Err(std::io::Error::other(e)),
63 },
64 Err(e) => return Err(std::io::Error::other(e)),
65 };
66 match parent.virtual_join(link_ref) {
67 Ok(p) => p,
68 Err(e) => return Err(std::io::Error::other(e)),
69 }
70 };
71
72 self.inner.strict_symlink(validated_link.inner.path())
73 }
74
75 /// Read the target of a symbolic link and return it as a validated `VirtualPath`.
76 ///
77 /// DESIGN NOTE:
78 /// This method has limited practical use because `virtual_join` resolves symlinks
79 /// during canonicalization. A `VirtualPath` obtained via `virtual_join("/link")` already
80 /// points to the symlink's target, not the symlink itself.
81 ///
82 /// To read a symlink target before validation, use `std::fs::read_link` on the raw
83 /// path, then validate the target with `virtual_join`:
84 ///
85 /// # Examples
86 ///
87 /// ```rust
88 /// use strict_path::VirtualRoot;
89 ///
90 /// let temp = tempfile::tempdir()?;
91 /// let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
92 ///
93 /// // Create a target file
94 /// let target = vroot.virtual_join("/data/target.txt")?;
95 /// target.create_parent_dir_all()?;
96 /// target.write("secret")?;
97 ///
98 /// // Create symlink (may fail on Windows without Developer Mode)
99 /// if target.virtual_symlink("/data/link.txt").is_ok() {
100 /// // virtual_join resolves symlinks: link.txt -> target.txt
101 /// let resolved = vroot.virtual_join("/data/link.txt")?;
102 /// assert_eq!(resolved.virtualpath_display().to_string(), "/data/target.txt");
103 /// // The resolved path reads the target file's content
104 /// assert_eq!(resolved.read_to_string()?, "secret");
105 /// }
106 /// # Ok::<(), Box<dyn std::error::Error>>(())
107 /// ```
108 pub fn virtual_read_link(&self) -> std::io::Result<Self> {
109 // Read the raw symlink target
110 let raw_target = std::fs::read_link(self.inner.path())?;
111
112 // If the target is relative, resolve it relative to the symlink's parent
113 let resolved_target = if raw_target.is_relative() {
114 match self.inner.path().parent() {
115 Some(parent) => parent.join(&raw_target),
116 None => raw_target,
117 }
118 } else {
119 raw_target
120 };
121
122 // Validate through virtual_join which clamps escapes
123 // We need to compute the relative path from the virtual root
124 let vroot = self.inner.boundary().clone().virtualize();
125 vroot
126 .virtual_join(resolved_target)
127 .map_err(std::io::Error::other)
128 }
129
130 /// Create a hard link at `link_path` pointing to this virtual path (same virtual root required).
131 ///
132 /// Both `self` (target) and `link_path` must be `VirtualPath` instances created via `virtual_join()`,
133 /// which ensures all paths are clamped to the virtual root. Absolute paths like `"/etc/data"`
134 /// passed to `virtual_join()` are automatically clamped to `vroot/etc/data`, ensuring hard links
135 /// cannot escape the virtual root boundary.
136 ///
137 /// # Examples
138 ///
139 /// ```rust
140 /// # use strict_path::VirtualRoot;
141 /// # let td = tempfile::tempdir().unwrap();
142 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
143 ///
144 /// // Create target file
145 /// let target = vroot.virtual_join("/shared/data.dat")?;
146 /// target.create_parent_dir_all()?;
147 /// target.write(b"shared data")?;
148 ///
149 /// // Ensure link parent directory exists (Windows requires this for hard link creation)
150 /// let link = vroot.virtual_join("/backup/data.dat")?;
151 /// link.create_parent_dir_all()?;
152 ///
153 /// // Create hard link
154 /// target.virtual_hard_link("/backup/data.dat")?;
155 ///
156 /// // Read through link path, verify through target (hard link behavior)
157 /// link.write(b"modified")?;
158 /// assert_eq!(target.read_to_string()?, "modified");
159 /// # Ok::<(), Box<dyn std::error::Error>>(())
160 /// ```
161 pub fn virtual_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
162 let link_ref = link_path.as_ref();
163 let validated_link = if link_ref.is_absolute() {
164 match self.virtual_join(link_ref) {
165 Ok(p) => p,
166 Err(e) => return Err(std::io::Error::other(e)),
167 }
168 } else {
169 // Resolve as sibling
170 let parent = match self.virtualpath_parent() {
171 Ok(Some(p)) => p,
172 Ok(None) => match self
173 .inner
174 .boundary()
175 .clone()
176 .virtualize()
177 .into_virtualpath()
178 {
179 Ok(root) => root,
180 Err(e) => return Err(std::io::Error::other(e)),
181 },
182 Err(e) => return Err(std::io::Error::other(e)),
183 };
184 match parent.virtual_join(link_ref) {
185 Ok(p) => p,
186 Err(e) => return Err(std::io::Error::other(e)),
187 }
188 };
189
190 self.inner.strict_hard_link(validated_link.inner.path())
191 }
192
193 /// Create a Windows NTFS directory junction at `link_path` pointing to this virtual path.
194 ///
195 /// - Windows-only and behind the `junctions` feature.
196 /// - Directory-only semantics; both paths must share the same virtual root.
197 #[cfg(all(windows, feature = "junctions"))]
198 pub fn virtual_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
199 // Mirror virtual semantics used by symlink/hard-link helpers:
200 // - Absolute paths are interpreted in the VIRTUAL namespace and clamped to this root
201 // - Relative paths are resolved as siblings (or from the virtual root when at root)
202 let link_ref = link_path.as_ref();
203 let validated_link = if link_ref.is_absolute() {
204 match self.virtual_join(link_ref) {
205 Ok(p) => p,
206 Err(e) => return Err(std::io::Error::other(e)),
207 }
208 } else {
209 let parent = match self.virtualpath_parent() {
210 Ok(Some(p)) => p,
211 Ok(None) => match self
212 .inner
213 .boundary()
214 .clone()
215 .virtualize()
216 .into_virtualpath()
217 {
218 Ok(root) => root,
219 Err(e) => return Err(std::io::Error::other(e)),
220 },
221 Err(e) => return Err(std::io::Error::other(e)),
222 };
223 match parent.virtual_join(link_ref) {
224 Ok(p) => p,
225 Err(e) => return Err(std::io::Error::other(e)),
226 }
227 };
228
229 // Delegate to strict helper after validating link location in virtual space
230 self.inner.strict_junction(validated_link.inner.path())
231 }
232
233 /// Rename/move within the same virtual root. Relative destinations are siblings; absolute are clamped to root.
234 ///
235 /// Accepts `impl AsRef<Path>` for the destination. Absolute paths (starting with `"/"`) are
236 /// automatically clamped to the virtual root via internal `virtual_join()` call, ensuring the
237 /// destination cannot escape the virtual boundary. Relative paths are resolved as siblings.
238 /// Parent directories are not created automatically.
239 ///
240 /// # Examples
241 ///
242 /// ```rust
243 /// # use strict_path::VirtualRoot;
244 /// # let td = tempfile::tempdir().unwrap();
245 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
246 ///
247 /// let source = vroot.virtual_join("temp/file.txt")?;
248 /// source.create_parent_dir_all()?;
249 /// source.write(b"content")?;
250 ///
251 /// // Absolute destination path is clamped to virtual root
252 /// let dest_dir = vroot.virtual_join("/archive")?;
253 /// dest_dir.create_dir_all()?;
254 /// source.virtual_rename("/archive/file.txt")?;
255 ///
256 /// let renamed = vroot.virtual_join("/archive/file.txt")?;
257 /// assert_eq!(renamed.read_to_string()?, "content");
258 /// # Ok::<(), Box<dyn std::error::Error>>(())
259 /// ```
260 pub fn virtual_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
261 let dest_ref = dest.as_ref();
262 let dest_v = if dest_ref.is_absolute() {
263 match self.virtual_join(dest_ref) {
264 Ok(p) => p,
265 Err(e) => return Err(std::io::Error::other(e)),
266 }
267 } else {
268 // Resolve as sibling under the current virtual parent (or root if at "/")
269 let parent = match self.virtualpath_parent() {
270 Ok(Some(p)) => p,
271 Ok(None) => match self
272 .inner
273 .boundary()
274 .clone()
275 .virtualize()
276 .into_virtualpath()
277 {
278 Ok(root) => root,
279 Err(e) => return Err(std::io::Error::other(e)),
280 },
281 Err(e) => return Err(std::io::Error::other(e)),
282 };
283 match parent.virtual_join(dest_ref) {
284 Ok(p) => p,
285 Err(e) => return Err(std::io::Error::other(e)),
286 }
287 };
288
289 // Perform the actual rename via StrictPath
290 self.inner.strict_rename(dest_v.inner.path())
291 }
292
293 /// Copy within the same virtual root. Relative destinations are siblings; absolute are clamped to root.
294 ///
295 /// Accepts `impl AsRef<Path>` for the destination. Absolute paths (starting with `"/"`) are
296 /// automatically clamped to the virtual root via internal `virtual_join()` call, ensuring the
297 /// destination cannot escape the virtual boundary. Relative paths are resolved as siblings.
298 /// Parent directories are not created automatically. Returns the number of bytes copied.
299 ///
300 /// # Examples
301 ///
302 /// ```rust
303 /// # use strict_path::VirtualRoot;
304 /// # let td = tempfile::tempdir().unwrap();
305 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
306 ///
307 /// let source = vroot.virtual_join("data/source.txt")?;
308 /// source.create_parent_dir_all()?;
309 /// source.write(b"data to copy")?;
310 ///
311 /// // Absolute destination path is clamped to virtual root
312 /// let dest_dir = vroot.virtual_join("/backup")?;
313 /// dest_dir.create_dir_all()?;
314 /// let bytes = source.virtual_copy("/backup/copy.txt")?;
315 ///
316 /// let copied = vroot.virtual_join("/backup/copy.txt")?;
317 /// assert_eq!(copied.read_to_string()?, "data to copy");
318 /// assert_eq!(bytes, 12);
319 /// # Ok::<(), Box<dyn std::error::Error>>(())
320 /// ```
321 pub fn virtual_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<u64> {
322 let dest_ref = dest.as_ref();
323 let dest_v = if dest_ref.is_absolute() {
324 match self.virtual_join(dest_ref) {
325 Ok(p) => p,
326 Err(e) => return Err(std::io::Error::other(e)),
327 }
328 } else {
329 // Resolve as sibling under the current virtual parent (or root if at "/")
330 let parent = match self.virtualpath_parent() {
331 Ok(Some(p)) => p,
332 Ok(None) => match self
333 .inner
334 .boundary()
335 .clone()
336 .virtualize()
337 .into_virtualpath()
338 {
339 Ok(root) => root,
340 Err(e) => return Err(std::io::Error::other(e)),
341 },
342 Err(e) => return Err(std::io::Error::other(e)),
343 };
344 match parent.virtual_join(dest_ref) {
345 Ok(p) => p,
346 Err(e) => return Err(std::io::Error::other(e)),
347 }
348 };
349
350 // Perform the actual copy via StrictPath
351 std::fs::copy(self.inner.path(), dest_v.inner.path())
352 }
353}