Skip to main content

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}