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}