Skip to main content

strict_path/path/virtual_path/
links.rs

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