Skip to main content

zerobox_utils_absolute_path/
lib.rs

1use dirs::home_dir;
2use schemars::JsonSchema;
3use serde::Deserialize;
4use serde::Deserializer;
5use serde::Serialize;
6use serde::de::Error as SerdeError;
7use std::borrow::Cow;
8use std::cell::RefCell;
9use std::path::Display;
10use std::path::Path;
11use std::path::PathBuf;
12use ts_rs::TS;
13
14mod absolutize;
15
16/// A path that is guaranteed to be absolute and normalized (though it is not
17/// guaranteed to be canonicalized or exist on the filesystem).
18///
19/// IMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set
20/// using [AbsolutePathBufGuard::new]. If no base path is set, the
21/// deserialization will fail unless the path being deserialized is already
22/// absolute.
23#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, JsonSchema, TS)]
24pub struct AbsolutePathBuf(PathBuf);
25
26impl AbsolutePathBuf {
27    fn maybe_expand_home_directory(path: &Path) -> PathBuf {
28        if let Some(path_str) = path.to_str()
29            && let Some(home) = home_dir()
30            && let Some(rest) = path_str.strip_prefix('~')
31        {
32            if rest.is_empty() {
33                return home;
34            } else if let Some(rest) = rest.strip_prefix('/') {
35                return home.join(rest.trim_start_matches('/'));
36            } else if cfg!(windows)
37                && let Some(rest) = rest.strip_prefix('\\')
38            {
39                return home.join(rest.trim_start_matches('\\'));
40            }
41        }
42        path.to_path_buf()
43    }
44
45    pub fn resolve_path_against_base<P: AsRef<Path>, B: AsRef<Path>>(
46        path: P,
47        base_path: B,
48    ) -> Self {
49        let expanded = Self::maybe_expand_home_directory(path.as_ref());
50        let expanded = normalize_path_for_platform(&expanded);
51        let base_path = normalize_path_for_platform(base_path.as_ref());
52        Self(absolutize::absolutize_from(
53            expanded.as_ref(),
54            base_path.as_ref(),
55        ))
56    }
57
58    pub fn from_absolute_path<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
59        let expanded = Self::maybe_expand_home_directory(path.as_ref());
60        let expanded = normalize_path_for_platform(&expanded);
61        Ok(Self(absolutize::absolutize(expanded.as_ref())?))
62    }
63
64    pub fn from_absolute_path_checked<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
65        let expanded = Self::maybe_expand_home_directory(path.as_ref());
66        let expanded = normalize_path_for_platform(&expanded);
67        if !expanded.is_absolute() {
68            return Err(std::io::Error::new(
69                std::io::ErrorKind::InvalidInput,
70                format!("path is not absolute: {}", path.as_ref().display()),
71            ));
72        }
73
74        Ok(Self(absolutize::absolutize_from(
75            expanded.as_ref(),
76            Path::new("/"),
77        )))
78    }
79
80    pub fn current_dir() -> std::io::Result<Self> {
81        Self::from_absolute_path(std::env::current_dir()?)
82    }
83
84    /// Construct an absolute path from `path`, resolving relative paths against
85    /// the process current working directory.
86    pub fn relative_to_current_dir<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
87        Ok(Self::resolve_path_against_base(
88            path,
89            std::env::current_dir()?,
90        ))
91    }
92
93    pub fn join<P: AsRef<Path>>(&self, path: P) -> Self {
94        Self::resolve_path_against_base(path, &self.0)
95    }
96
97    pub fn canonicalize(&self) -> std::io::Result<Self> {
98        dunce::canonicalize(&self.0).map(Self)
99    }
100
101    pub fn parent(&self) -> Option<Self> {
102        self.0.parent().map(|p| {
103            debug_assert!(
104                p.is_absolute(),
105                "parent of AbsolutePathBuf must be absolute"
106            );
107            Self(p.to_path_buf())
108        })
109    }
110
111    pub fn ancestors(&self) -> impl Iterator<Item = Self> + '_ {
112        self.0.ancestors().map(|p| {
113            debug_assert!(
114                p.is_absolute(),
115                "ancestor of AbsolutePathBuf must be absolute"
116            );
117            Self(p.to_path_buf())
118        })
119    }
120
121    pub fn as_path(&self) -> &Path {
122        &self.0
123    }
124
125    pub fn into_path_buf(self) -> PathBuf {
126        self.0
127    }
128
129    pub fn to_path_buf(&self) -> PathBuf {
130        self.0.clone()
131    }
132
133    pub fn to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
134        self.0.to_string_lossy()
135    }
136
137    pub fn display(&self) -> Display<'_> {
138        self.0.display()
139    }
140}
141
142fn normalize_path_for_platform(path: &Path) -> Cow<'_, Path> {
143    if cfg!(windows)
144        && let Some(path) = path.to_str()
145        && let Some(normalized) = normalize_windows_device_path(path)
146    {
147        return Cow::Owned(PathBuf::from(normalized));
148    }
149
150    Cow::Borrowed(path)
151}
152
153fn normalize_windows_device_path(path: &str) -> Option<String> {
154    if let Some(unc) = path.strip_prefix(r"\\?\UNC\") {
155        return Some(format!(r"\\{unc}"));
156    }
157    if let Some(unc) = path.strip_prefix(r"\\.\UNC\") {
158        return Some(format!(r"\\{unc}"));
159    }
160    if let Some(path) = path.strip_prefix(r"\\?\")
161        && is_windows_drive_absolute_path(path)
162    {
163        return Some(path.to_string());
164    }
165    if let Some(path) = path.strip_prefix(r"\\.\")
166        && is_windows_drive_absolute_path(path)
167    {
168        return Some(path.to_string());
169    }
170    None
171}
172
173fn is_windows_drive_absolute_path(path: &str) -> bool {
174    let bytes = path.as_bytes();
175    bytes.len() >= 3
176        && bytes[0].is_ascii_alphabetic()
177        && bytes[1] == b':'
178        && matches!(bytes[2], b'\\' | b'/')
179}
180
181/// Canonicalize a path when possible, but preserve the logical absolute path
182/// whenever canonicalization would rewrite it through a nested symlink.
183///
184/// Top-level system aliases such as macOS `/var -> /private/var` still remain
185/// canonicalized so existing runtime expectations around those paths stay
186/// stable. If the full path cannot be canonicalized, this returns the logical
187/// absolute path; use [`canonicalize_existing_preserving_symlinks`] for paths
188/// that must exist.
189pub fn canonicalize_preserving_symlinks(path: &Path) -> std::io::Result<PathBuf> {
190    let logical = AbsolutePathBuf::from_absolute_path(path)?.into_path_buf();
191    let preserve_logical_path = should_preserve_logical_path(&logical);
192    match dunce::canonicalize(path) {
193        Ok(canonical) if preserve_logical_path && canonical != logical => Ok(logical),
194        Ok(canonical) => Ok(canonical),
195        Err(_) => Ok(logical),
196    }
197}
198
199/// Canonicalize an existing path while preserving the logical absolute path
200/// whenever canonicalization would rewrite it through a nested symlink.
201///
202/// Unlike [`canonicalize_preserving_symlinks`], canonicalization failures are
203/// propagated so callers can reject invalid working directories early.
204pub fn canonicalize_existing_preserving_symlinks(path: &Path) -> std::io::Result<PathBuf> {
205    let logical = AbsolutePathBuf::from_absolute_path(path)?.into_path_buf();
206    let canonical = dunce::canonicalize(path)?;
207    if should_preserve_logical_path(&logical) && canonical != logical {
208        Ok(logical)
209    } else {
210        Ok(canonical)
211    }
212}
213
214fn should_preserve_logical_path(logical: &Path) -> bool {
215    logical.ancestors().any(|ancestor| {
216        let Ok(metadata) = std::fs::symlink_metadata(ancestor) else {
217            return false;
218        };
219        metadata.file_type().is_symlink() && ancestor.parent().and_then(Path::parent).is_some()
220    })
221}
222
223impl AsRef<Path> for AbsolutePathBuf {
224    fn as_ref(&self) -> &Path {
225        &self.0
226    }
227}
228
229impl std::ops::Deref for AbsolutePathBuf {
230    type Target = Path;
231
232    fn deref(&self) -> &Self::Target {
233        &self.0
234    }
235}
236
237impl From<AbsolutePathBuf> for PathBuf {
238    fn from(path: AbsolutePathBuf) -> Self {
239        path.into_path_buf()
240    }
241}
242
243/// Helpers for constructing absolute paths in tests.
244pub mod test_support {
245    use super::AbsolutePathBuf;
246    use std::path::Path;
247    use std::path::PathBuf;
248
249    /// Creates a platform-absolute [`PathBuf`] from a Unix-style absolute test path.
250    ///
251    /// On Windows, `/tmp/example` maps to `C:\tmp\example`.
252    pub fn test_path_buf(unix_path: &str) -> PathBuf {
253        if cfg!(windows) {
254            let mut path = PathBuf::from(r"C:\");
255            path.extend(
256                unix_path
257                    .trim_start_matches('/')
258                    .split('/')
259                    .filter(|segment| !segment.is_empty()),
260            );
261            path
262        } else {
263            PathBuf::from(unix_path)
264        }
265    }
266
267    /// Extension methods for converting paths into [`AbsolutePathBuf`] values in tests.
268    pub trait PathExt {
269        /// Converts an already absolute path into an [`AbsolutePathBuf`].
270        fn abs(&self) -> AbsolutePathBuf;
271    }
272
273    impl PathExt for Path {
274        #[expect(clippy::expect_used)]
275        fn abs(&self) -> AbsolutePathBuf {
276            AbsolutePathBuf::from_absolute_path_checked(self)
277                .expect("path should already be absolute")
278        }
279    }
280
281    /// Extension methods for converting path buffers into [`AbsolutePathBuf`] values in tests.
282    pub trait PathBufExt {
283        /// Converts an already absolute path buffer into an [`AbsolutePathBuf`].
284        fn abs(&self) -> AbsolutePathBuf;
285    }
286
287    impl PathBufExt for PathBuf {
288        fn abs(&self) -> AbsolutePathBuf {
289            self.as_path().abs()
290        }
291    }
292}
293
294impl TryFrom<&Path> for AbsolutePathBuf {
295    type Error = std::io::Error;
296
297    fn try_from(value: &Path) -> Result<Self, Self::Error> {
298        Self::from_absolute_path(value)
299    }
300}
301
302impl TryFrom<PathBuf> for AbsolutePathBuf {
303    type Error = std::io::Error;
304
305    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
306        Self::from_absolute_path(value)
307    }
308}
309
310impl TryFrom<&str> for AbsolutePathBuf {
311    type Error = std::io::Error;
312
313    fn try_from(value: &str) -> Result<Self, Self::Error> {
314        Self::from_absolute_path(value)
315    }
316}
317
318impl TryFrom<String> for AbsolutePathBuf {
319    type Error = std::io::Error;
320
321    fn try_from(value: String) -> Result<Self, Self::Error> {
322        Self::from_absolute_path(value)
323    }
324}
325
326thread_local! {
327    static ABSOLUTE_PATH_BASE: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
328}
329
330/// Ensure this guard is held while deserializing `AbsolutePathBuf` values to
331/// provide a base path for resolving relative paths. Because this relies on
332/// thread-local storage, the deserialization must be single-threaded and
333/// occur on the same thread that created the guard.
334pub struct AbsolutePathBufGuard;
335
336impl AbsolutePathBufGuard {
337    pub fn new(base_path: &Path) -> Self {
338        ABSOLUTE_PATH_BASE.with(|cell| {
339            *cell.borrow_mut() = Some(base_path.to_path_buf());
340        });
341        Self
342    }
343}
344
345impl Drop for AbsolutePathBufGuard {
346    fn drop(&mut self) {
347        ABSOLUTE_PATH_BASE.with(|cell| {
348            *cell.borrow_mut() = None;
349        });
350    }
351}
352
353impl<'de> Deserialize<'de> for AbsolutePathBuf {
354    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
355    where
356        D: Deserializer<'de>,
357    {
358        let path = PathBuf::deserialize(deserializer)?;
359        ABSOLUTE_PATH_BASE.with(|cell| match cell.borrow().as_deref() {
360            Some(base) => Ok(Self::resolve_path_against_base(path, base)),
361            None if path.is_absolute() => {
362                Self::from_absolute_path(path).map_err(SerdeError::custom)
363            }
364            None => Err(SerdeError::custom(
365                "AbsolutePathBuf deserialized without a base path",
366            )),
367        })
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::test_support::test_path_buf;
375    use pretty_assertions::assert_eq;
376    use std::fs;
377    #[cfg(unix)]
378    use std::process::Command;
379    use tempfile::tempdir;
380
381    #[test]
382    fn create_with_absolute_path_ignores_base_path() {
383        let base_dir = tempdir().expect("base dir");
384        let absolute_dir = tempdir().expect("absolute dir");
385        let base_path = base_dir.path();
386        let absolute_path = absolute_dir.path().join("file.txt");
387        let abs_path_buf =
388            AbsolutePathBuf::resolve_path_against_base(absolute_path.clone(), base_path);
389        assert_eq!(abs_path_buf.as_path(), absolute_path.as_path());
390    }
391
392    #[cfg(unix)]
393    #[test]
394    fn from_absolute_path_does_not_read_current_dir_when_path_is_absolute() {
395        let status = Command::new(std::env::current_exe().expect("current test binary"))
396            .arg("from_absolute_path_with_removed_current_dir_child")
397            .arg("--ignored")
398            .env("CODEX_ABSOLUTE_PATH_REMOVED_CWD_CHILD", "1")
399            .status()
400            .expect("run child test");
401
402        assert!(status.success());
403    }
404
405    #[cfg(unix)]
406    #[test]
407    #[ignore]
408    fn from_absolute_path_with_removed_current_dir_child() {
409        if std::env::var_os("CODEX_ABSOLUTE_PATH_REMOVED_CWD_CHILD").is_none() {
410            return;
411        }
412
413        let original_cwd = std::env::current_dir().expect("original cwd");
414        let temp_dir = tempdir().expect("temp dir");
415        let removed_cwd = temp_dir.path().to_path_buf();
416        std::env::set_current_dir(&removed_cwd).expect("enter temp dir");
417        std::fs::remove_dir(&removed_cwd).expect("remove current dir");
418        std::env::current_dir().expect_err("current dir should be unavailable");
419
420        let path = AbsolutePathBuf::from_absolute_path(test_path_buf(
421            "/tmp/codex/../codex-home/plugins/cache",
422        ))
423        .expect("absolute path should not require current dir");
424
425        std::env::set_current_dir(original_cwd).expect("restore cwd");
426        assert_eq!(
427            path.as_path(),
428            test_path_buf("/tmp/codex-home/plugins/cache")
429        );
430    }
431
432    #[test]
433    fn from_absolute_path_checked_rejects_relative_path() {
434        let err = AbsolutePathBuf::from_absolute_path_checked("relative/path")
435            .expect_err("relative path should fail");
436
437        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
438    }
439
440    #[test]
441    fn normalize_windows_device_path_strips_supported_verbatim_prefixes() {
442        assert_eq!(
443            normalize_windows_device_path(r"\\?\D:\c\x\worktrees\2508\swift-base"),
444            Some(r"D:\c\x\worktrees\2508\swift-base".to_string())
445        );
446        assert_eq!(
447            normalize_windows_device_path(r"\\.\D:\c\x\worktrees\2508\swift-base"),
448            Some(r"D:\c\x\worktrees\2508\swift-base".to_string())
449        );
450        assert_eq!(
451            normalize_windows_device_path(r"\\?\UNC\server\share\workspace"),
452            Some(r"\\server\share\workspace".to_string())
453        );
454        assert_eq!(
455            normalize_windows_device_path(r"\\.\UNC\server\share\workspace"),
456            Some(r"\\server\share\workspace".to_string())
457        );
458        assert_eq!(
459            normalize_windows_device_path(r"\\?\GLOBALROOT\Device"),
460            None
461        );
462    }
463
464    #[cfg(target_os = "windows")]
465    #[test]
466    fn from_absolute_path_strips_windows_verbatim_prefix() {
467        let path =
468            AbsolutePathBuf::from_absolute_path_checked(r"\\?\D:\c\x\worktrees\2508\swift-base")
469                .expect("verbatim drive path should be absolute");
470
471        assert_eq!(
472            path.as_path(),
473            Path::new(r"D:\c\x\worktrees\2508\swift-base")
474        );
475    }
476
477    #[test]
478    fn relative_path_is_resolved_against_base_path() {
479        let temp_dir = tempdir().expect("base dir");
480        let base_dir = temp_dir.path();
481        let abs_path_buf = AbsolutePathBuf::resolve_path_against_base("file.txt", base_dir);
482        assert_eq!(abs_path_buf.as_path(), base_dir.join("file.txt").as_path());
483    }
484
485    #[test]
486    fn relative_path_dots_are_normalized_against_base_path() {
487        let temp_dir = tempdir().expect("base dir");
488        let base_dir = temp_dir.path();
489        let abs_path_buf =
490            AbsolutePathBuf::resolve_path_against_base("./nested/../file.txt", base_dir);
491        assert_eq!(abs_path_buf.as_path(), base_dir.join("file.txt").as_path());
492    }
493
494    #[test]
495    fn canonicalize_returns_absolute_path_buf() {
496        let temp_dir = tempdir().expect("base dir");
497        fs::create_dir(temp_dir.path().join("one")).expect("create one dir");
498        fs::create_dir(temp_dir.path().join("two")).expect("create two dir");
499        fs::write(temp_dir.path().join("two").join("file.txt"), "").expect("write file");
500        let abs_path_buf =
501            AbsolutePathBuf::from_absolute_path(temp_dir.path().join("one/../two/./file.txt"))
502                .expect("absolute path");
503        assert_eq!(
504            abs_path_buf
505                .canonicalize()
506                .expect("path should canonicalize")
507                .as_path(),
508            dunce::canonicalize(temp_dir.path().join("two").join("file.txt"))
509                .expect("expected path should canonicalize")
510                .as_path()
511        );
512    }
513
514    #[test]
515    fn canonicalize_returns_error_for_missing_path() {
516        let temp_dir = tempdir().expect("base dir");
517        let abs_path_buf = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("missing.txt"))
518            .expect("absolute path");
519
520        assert!(abs_path_buf.canonicalize().is_err());
521    }
522
523    #[test]
524    fn ancestors_returns_absolute_path_bufs() {
525        let abs_path_buf =
526            AbsolutePathBuf::from_absolute_path_checked(test_path_buf("/tmp/one/two"))
527                .expect("absolute path");
528
529        let ancestors = abs_path_buf
530            .ancestors()
531            .map(|path| path.to_path_buf())
532            .collect::<Vec<_>>();
533
534        let expected = vec![
535            test_path_buf("/tmp/one/two"),
536            test_path_buf("/tmp/one"),
537            test_path_buf("/tmp"),
538            test_path_buf("/"),
539        ];
540
541        assert_eq!(ancestors, expected);
542    }
543
544    #[test]
545    fn relative_to_current_dir_resolves_relative_path() -> std::io::Result<()> {
546        let current_dir = std::env::current_dir()?;
547        let abs_path_buf = AbsolutePathBuf::relative_to_current_dir("file.txt")?;
548        assert_eq!(
549            abs_path_buf.as_path(),
550            current_dir.join("file.txt").as_path()
551        );
552        Ok(())
553    }
554
555    #[test]
556    fn guard_used_in_deserialization() {
557        let temp_dir = tempdir().expect("base dir");
558        let base_dir = temp_dir.path();
559        let relative_path = "subdir/file.txt";
560        let abs_path_buf = {
561            let _guard = AbsolutePathBufGuard::new(base_dir);
562            serde_json::from_str::<AbsolutePathBuf>(&format!(r#""{relative_path}""#))
563                .expect("failed to deserialize")
564        };
565        assert_eq!(
566            abs_path_buf.as_path(),
567            base_dir.join(relative_path).as_path()
568        );
569    }
570
571    #[test]
572    fn home_directory_root_is_expanded_in_deserialization() {
573        let Some(home) = home_dir() else {
574            return;
575        };
576        let temp_dir = tempdir().expect("base dir");
577        let abs_path_buf = {
578            let _guard = AbsolutePathBufGuard::new(temp_dir.path());
579            serde_json::from_str::<AbsolutePathBuf>("\"~\"").expect("failed to deserialize")
580        };
581        assert_eq!(abs_path_buf.as_path(), home.as_path());
582    }
583
584    #[test]
585    fn home_directory_subpath_is_expanded_in_deserialization() {
586        let Some(home) = home_dir() else {
587            return;
588        };
589        let temp_dir = tempdir().expect("base dir");
590        let abs_path_buf = {
591            let _guard = AbsolutePathBufGuard::new(temp_dir.path());
592            serde_json::from_str::<AbsolutePathBuf>("\"~/code\"").expect("failed to deserialize")
593        };
594        assert_eq!(abs_path_buf.as_path(), home.join("code").as_path());
595    }
596
597    #[test]
598    fn home_directory_double_slash_is_expanded_in_deserialization() {
599        let Some(home) = home_dir() else {
600            return;
601        };
602        let temp_dir = tempdir().expect("base dir");
603        let abs_path_buf = {
604            let _guard = AbsolutePathBufGuard::new(temp_dir.path());
605            serde_json::from_str::<AbsolutePathBuf>("\"~//code\"").expect("failed to deserialize")
606        };
607        assert_eq!(abs_path_buf.as_path(), home.join("code").as_path());
608    }
609
610    #[cfg(unix)]
611    #[test]
612    fn canonicalize_preserving_symlinks_keeps_logical_symlink_path() {
613        let temp_dir = tempdir().expect("temp dir");
614        let real = temp_dir.path().join("real");
615        let link = temp_dir.path().join("link");
616        std::fs::create_dir_all(&real).expect("create real dir");
617        std::os::unix::fs::symlink(&real, &link).expect("create symlink");
618
619        let canonicalized =
620            canonicalize_preserving_symlinks(&link).expect("canonicalize preserving symlinks");
621
622        assert_eq!(canonicalized, link);
623    }
624
625    #[cfg(unix)]
626    #[test]
627    fn canonicalize_preserving_symlinks_keeps_logical_missing_child_under_symlink() {
628        let temp_dir = tempdir().expect("temp dir");
629        let real = temp_dir.path().join("real");
630        let link = temp_dir.path().join("link");
631        std::fs::create_dir_all(&real).expect("create real dir");
632        std::os::unix::fs::symlink(&real, &link).expect("create symlink");
633        let missing = link.join("missing.txt");
634
635        let canonicalized =
636            canonicalize_preserving_symlinks(&missing).expect("canonicalize preserving symlinks");
637
638        assert_eq!(canonicalized, missing);
639    }
640
641    #[test]
642    fn canonicalize_existing_preserving_symlinks_errors_for_missing_path() {
643        let temp_dir = tempdir().expect("temp dir");
644        let missing = temp_dir.path().join("missing");
645
646        let err = canonicalize_existing_preserving_symlinks(&missing)
647            .expect_err("missing path should fail canonicalization");
648
649        assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
650    }
651
652    #[cfg(unix)]
653    #[test]
654    fn canonicalize_existing_preserving_symlinks_keeps_logical_symlink_path() {
655        let temp_dir = tempdir().expect("temp dir");
656        let real = temp_dir.path().join("real");
657        let link = temp_dir.path().join("link");
658        std::fs::create_dir_all(&real).expect("create real dir");
659        std::os::unix::fs::symlink(&real, &link).expect("create symlink");
660
661        let canonicalized =
662            canonicalize_existing_preserving_symlinks(&link).expect("canonicalize symlink");
663
664        assert_eq!(canonicalized, link);
665    }
666
667    #[cfg(target_os = "windows")]
668    #[test]
669    fn home_directory_backslash_subpath_is_expanded_in_deserialization() {
670        let Some(home) = home_dir() else {
671            return;
672        };
673        let temp_dir = tempdir().expect("base dir");
674        let abs_path_buf = {
675            let _guard = AbsolutePathBufGuard::new(temp_dir.path());
676            let input =
677                serde_json::to_string(r#"~\code"#).expect("string should serialize as JSON");
678            serde_json::from_str::<AbsolutePathBuf>(&input).expect("is valid abs path")
679        };
680        assert_eq!(abs_path_buf.as_path(), home.join("code").as_path());
681    }
682
683    #[cfg(target_os = "windows")]
684    #[test]
685    fn canonicalize_preserving_symlinks_avoids_verbatim_prefixes() {
686        let temp_dir = tempdir().expect("temp dir");
687
688        let canonicalized =
689            canonicalize_preserving_symlinks(temp_dir.path()).expect("canonicalize");
690
691        assert_eq!(
692            canonicalized,
693            dunce::canonicalize(temp_dir.path()).expect("canonicalize temp dir")
694        );
695        assert!(
696            !canonicalized.to_string_lossy().starts_with(r"\\?\"),
697            "expected a non-verbatim Windows path, got {canonicalized:?}"
698        );
699    }
700}