/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

// We'd love to use fs-err instead, but that code gives bad error messages and doesn't wrap all functions.
// Various bugs have been raised - if they all get fixed we can migrate.
use std::borrow::Cow;
use std::env;
use std::fs;
use std::fs::File;
use std::io;
use std::io::Read;
use std::io::Write;
use std::ops::Deref;
use std::path::Path;
use std::path::PathBuf;

use anyhow::Context as _;
use relative_path::RelativePath;
use relative_path::RelativePathBuf;

use crate::fs::cwd::assert_cwd_is_not_set;
use crate::fs::paths::abs_norm_path::AbsNormPath;
use crate::fs::paths::abs_norm_path::AbsNormPathBuf;
use crate::fs::paths::abs_path::AbsPath;
use crate::io_counters::IoCounterGuard;
use crate::io_counters::IoCounterKey;

pub fn symlink<P, Q>(original: P, link: Q) -> anyhow::Result<()>
where
    P: AsRef<Path>,
    Q: AsRef<AbsPath>,
{
    let _guard = IoCounterKey::Symlink.guard();
    symlink_impl(original.as_ref(), link.as_ref()).with_context(|| {
        format!(
            "symlink(original={}, link={})",
            original.as_ref().display(),
            link.as_ref().display()
        )
    })
}

#[cfg(unix)]
fn symlink_impl(original: &Path, link: &AbsPath) -> anyhow::Result<()> {
    std::os::unix::fs::symlink(original, link.as_maybe_relativized()).map_err(|e| e.into())
}

/// Create symlink on Windows.
#[cfg(windows)]
fn symlink_impl(original: &Path, link: &AbsPath) -> anyhow::Result<()> {
    use std::io::ErrorKind;

    use common_path::common_path;

    fn permission_check(result: io::Result<()>) -> anyhow::Result<()> {
        match result {
            // Standard issue on Windows machines, so hint at the resolution, as it is not obvious.
            // Unfortunately this doesn't have an `ErrorKind`, so have to do it with substring matching.
            Err(e) if e.to_string().contains("privilege is not held") => Err(anyhow::anyhow!(e)
                .context(
                    "Perhaps you need to turn on 'Developer Mode' in Windows to enable symlinks.",
                )),
            Err(e) => Err(e.into()),
            Ok(_) => Ok(()),
        }
    }

    let link = link.as_path();

    // If original is a relative path, fix it up to be absolute
    let target_abspath = if original.is_absolute() {
        Cow::Borrowed(original)
    } else {
        Cow::Owned(
            link.parent()
                .ok_or_else(|| anyhow::anyhow!("Expected path with a parent in symlink target"))?
                .join(original),
        )
    };
    let target_abspath = std::path::absolute(&target_abspath)?;

    // Relative symlinks in Windows are relative from the real/canonical path of the link,
    // so it can get messy when the symlink lives in buck-out. For Windows, we'll canonicalize,
    // or if the target doesn't exist yet simulate canonicalize() by canonicalizing the common
    // ancestor between the target and link and appending the rest. The common ancestor should
    // live somewhere between repo root / buck-out and link's directory, which should both exist.
    // Canonicalize() will also handle adding the verbatim prefix \\?\, which is required for
    // supporting paths longer than 260
    // In general, it should be OK to opt for absolute / canonical paths when possible as
    // buck will not read any of these paths.
    let target_canonical = if let Ok(path) = target_abspath.canonicalize() {
        path
    } else {
        // target doesn't exist yet, try to guess the canonical path
        if let Some(common_path) = common_path(&target_abspath, &link) {
            let from_common = target_abspath.strip_prefix(&common_path)?;
            let common_canonicalized = common_path
                .canonicalize()
                .context(format!("Failed to get canonical path of {:?}", common_path))?;
            common_canonicalized.join(from_common)
        } else {
            target_abspath
        }
    };

    let target_metadata = target_canonical.metadata();
    match target_metadata {
        Ok(meta) if meta.is_dir() => {
            permission_check(std::os::windows::fs::symlink_dir(&target_canonical, link))
        }
        Err(e) if e.kind() != ErrorKind::NotFound => Err(e.into()),
        _ => {
            // Either file or not existent. Default to file.
            // TODO(T144443238): This will cause issues if the file type turns out to be directory, fix this
            permission_check(std::os::windows::fs::symlink_file(&target_canonical, link))
        }
    }
}

pub fn set_current_dir<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<()> {
    assert_cwd_is_not_set()?;
    env::set_current_dir(path.as_ref())
        .with_context(|| format!("set_current_dir({})", P::as_ref(&path).display()))
}

pub fn create_dir_all<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<()> {
    let _guard = IoCounterKey::MkDir.guard();
    fs::create_dir_all(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("create_dir_all({})", P::as_ref(&path).display()))?;
    Ok(())
}

pub fn create_dir<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<()> {
    let _guard = IoCounterKey::MkDir.guard();
    fs::create_dir(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("create_dir({})", P::as_ref(&path).display()))?;
    Ok(())
}

/// Create directory if not exists.
///
/// Fail if exists but is not a directory or creation failed.
pub fn create_dir_if_not_exists<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<()> {
    let path = path.as_ref();
    let _guard = IoCounterKey::MkDir.guard();
    let e = match fs::create_dir(path.as_maybe_relativized())
        .with_context(|| format!("create_dir({})", path.display()))
    {
        Ok(()) => return Ok(()),
        Err(e) => e,
    };

    match symlink_metadata(path) {
        Ok(metadata) => {
            if metadata.is_dir() {
                Ok(())
            } else {
                // File exists but not a directory, return original error.
                Err(e)
            }
        }
        Err(_) => {
            // `lstat` failed, means something like permission denied, return original error.
            Err(e)
        }
    }
}

/// `DirEntry` which is known to contain absolute path.
pub struct DirEntry {
    dir_entry: fs::DirEntry,
}

impl DirEntry {
    pub fn path(&self) -> AbsNormPathBuf {
        // This is safe, because `read_dir` is called with absolute path,
        // and filename is not dot or dot-dot.
        AbsNormPathBuf::unchecked_new(self.dir_entry.path())
    }
}

impl Deref for DirEntry {
    type Target = fs::DirEntry;

    fn deref(&self) -> &Self::Target {
        &self.dir_entry
    }
}

pub struct ReadDir {
    read_dir: fs::ReadDir,
    /// We store guard in the iterator, so if an iteration does non-trivial operations,
    /// these non-trivial operations will be considered part of read dir in IO counters.
    _guard: IoCounterGuard,
}

impl Iterator for ReadDir {
    type Item = io::Result<DirEntry>;

    fn next(&mut self) -> Option<Self::Item> {
        self.read_dir
            .next()
            .map(|res| res.map(|dir_entry| DirEntry { dir_entry }))
    }
}

pub fn read_dir<P: AsRef<AbsNormPath>>(path: P) -> anyhow::Result<ReadDir> {
    let _guard = IoCounterKey::ReadDir.guard();
    fs::read_dir(path.as_ref())
        .with_context(|| format!("read_dir({})", P::as_ref(&path).display()))
        .map(|read_dir| ReadDir { read_dir, _guard })
}

pub fn read_dir_if_exists<P: AsRef<AbsNormPath>>(path: P) -> anyhow::Result<Option<ReadDir>> {
    let _guard = IoCounterKey::ReadDir.guard();
    let read_dir = fs::read_dir(path.as_ref());
    let read_dir = match read_dir {
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(e) => {
            return Err(e)
                .with_context(|| format!("read_dir_if_exists({})", P::as_ref(&path).display()));
        }
        Ok(x) => x,
    };
    Ok(Some(ReadDir { read_dir, _guard }))
}

pub fn try_exists<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<bool> {
    let _guard = IoCounterKey::Stat.guard();
    fs::try_exists(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("try_exists({})", P::as_ref(&path).display()))
}

pub fn remove_file<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<()> {
    let _guard = IoCounterKey::Remove.guard();
    remove_file_impl(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("remove_file({})", P::as_ref(&path).display()))
}

#[cfg(unix)]
fn remove_file_impl(path: &Path) -> anyhow::Result<()> {
    fs::remove_file(path)?;
    Ok(())
}

#[cfg(windows)]
fn remove_file_impl(path: &Path) -> anyhow::Result<()> {
    use std::os::windows::fs::FileTypeExt;

    let file_type = path.symlink_metadata()?.file_type();
    if !file_type.is_symlink() || file_type.is_symlink_file() {
        fs::remove_file(path)?;
    } else {
        fs::remove_dir(path)?;
    }
    Ok(())
}

pub fn copy<P: AsRef<AbsPath>, Q: AsRef<AbsPath>>(from: P, to: Q) -> anyhow::Result<u64> {
    let _guard = IoCounterKey::Copy.guard();
    fs::copy(
        from.as_ref().as_maybe_relativized(),
        to.as_ref().as_maybe_relativized(),
    )
    .with_context(|| {
        format!(
            "copy(from={}, to={})",
            P::as_ref(&from).display(),
            Q::as_ref(&to).display()
        )
    })
}

pub fn read_link<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<PathBuf> {
    let _guard = IoCounterKey::ReadLink.guard();
    fs::read_link(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("read_link({})", P::as_ref(&path).display()))
}

pub fn rename<P: AsRef<AbsPath>, Q: AsRef<AbsPath>>(from: P, to: Q) -> anyhow::Result<()> {
    let _guard = IoCounterKey::Rename.guard();
    fs::rename(
        from.as_ref().as_maybe_relativized(),
        to.as_ref().as_maybe_relativized(),
    )
    .with_context(|| {
        format!(
            "rename(from={}, to={})",
            P::as_ref(&from).display(),
            Q::as_ref(&to).display()
        )
    })?;
    Ok(())
}

pub fn write<P: AsRef<AbsPath>, C: AsRef<[u8]>>(path: P, contents: C) -> anyhow::Result<()> {
    let _guard = IoCounterKey::Write.guard();
    fs::write(path.as_ref().as_maybe_relativized(), &contents)
        .with_context(|| format!("write({}, _)", P::as_ref(&path).display()))?;
    Ok(())
}

pub fn metadata<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<fs::Metadata> {
    let _guard = IoCounterKey::Stat.guard();
    fs::metadata(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("metadata({})", P::as_ref(&path).display()))
}

pub fn symlink_metadata<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<fs::Metadata> {
    let _guard = IoCounterKey::Stat.guard();
    fs::symlink_metadata(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("symlink_metadata({})", P::as_ref(&path).display()))
}

pub fn set_permissions<P: AsRef<AbsPath>>(path: P, perm: fs::Permissions) -> anyhow::Result<()> {
    let _guard = IoCounterKey::Chmod.guard();
    fs::set_permissions(path.as_ref().as_maybe_relativized(), perm)
        .with_context(|| format!("set_permissions({}, _)", P::as_ref(&path).display()))?;
    Ok(())
}

pub fn set_executable<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<()> {
    let path = path.as_ref();

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;

        // Unix permission bits
        let mut perms = metadata(path)?.permissions();
        // Add ugo+x
        perms.set_mode(perms.mode() | 0o111);
        set_permissions(path, perms)?;
    }
    #[cfg(not(unix))]
    {
        // Nothing to do
        let _ignore = path;
    }

    Ok(())
}

pub fn remove_dir_all<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<()> {
    let _guard = IoCounterKey::RmDirAll.guard();
    fs::remove_dir_all(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("remove_dir_all({})", P::as_ref(&path).display()))?;
    Ok(())
}

/// `None` if file does not exist.
pub fn symlink_metadata_if_exists<P: AsRef<AbsPath>>(
    path: P,
) -> anyhow::Result<Option<fs::Metadata>> {
    let _guard = IoCounterKey::Stat.guard();
    match fs::symlink_metadata(path.as_ref().as_maybe_relativized()) {
        Ok(metadata) => Ok(Some(metadata)),
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
        Err(err) => {
            Err(err).with_context(|| format!("symlink_metadata({})", path.as_ref().display()))
        }
    }
}

/// Like fs::exists but gives you the metadata. More efficient than calling `symlink_metadata().ok()` (no anyhow, no backtrace) or `exists()` (one stat call).
pub fn symlink_metadata_if_available<P: AsRef<AbsPath>>(path: P) -> Option<fs::Metadata> {
    let _guard = IoCounterKey::Stat.guard();
    fs::symlink_metadata(path.as_ref().as_maybe_relativized()).ok()
}

/// Remove whatever exists at `path`, be it a file, directory, pipe, broken symlink, etc.
/// Do nothing if `path` does not exist.
pub fn remove_all<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<()> {
    let guard = IoCounterKey::RmDirAll.guard();
    let metadata = match symlink_metadata_if_exists(&path)? {
        Some(s) => s,
        None => return Ok(()),
    };

    drop(guard);

    let r = if metadata.is_dir() {
        remove_dir_all(&path)
    } else {
        remove_file(&path)
    };
    if r.is_err() && symlink_metadata_if_exists(&path)?.is_none() {
        // Other process removed it, our goal is achieved.
        return Ok(());
    }
    r
}

pub fn read<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<Vec<u8>> {
    let _guard = IoCounterKey::Read.guard();
    fs::read(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("read({})", P::as_ref(&path).display()))
}

pub fn read_to_string<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<String> {
    let _guard = IoCounterKey::Read.guard();
    fs::read_to_string(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("read_to_string({})", P::as_ref(&path).display()))
}

/// Read a file, if it exists. Returns `None` when the file does not exist.
pub fn read_to_string_opt<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<Option<String>> {
    let _guard = IoCounterKey::Read.guard();
    match fs::read_to_string(path.as_ref().as_maybe_relativized()) {
        Ok(d) => Ok(Some(d)),
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
        Err(e) => Err(anyhow::Error::from(e).context(format!(
            "read_to_string_opt({})",
            P::as_ref(&path).display()
        ))),
    }
}

pub fn canonicalize<P: AsRef<Path>>(path: P) -> anyhow::Result<AbsNormPathBuf> {
    let _guard = IoCounterKey::Canonicalize.guard();
    let path = dunce::canonicalize(&path)
        .with_context(|| format!("canonicalize({})", P::as_ref(&path).display()))?;
    AbsNormPathBuf::new(path)
}

pub fn canonicalize_if_exists<P: AsRef<Path>>(path: P) -> anyhow::Result<Option<AbsNormPathBuf>> {
    let _guard = IoCounterKey::Canonicalize.guard();
    match dunce::canonicalize(&path) {
        Ok(path) => Some(AbsNormPathBuf::new(path)).transpose(),
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
        Err(e) => Err(anyhow::Error::new(e).context(format!(
            "canonicalize_if_exists({})",
            P::as_ref(&path).display()
        ))),
    }
}

/// Convert Windows UNC path to regular path.
pub fn simplified(path: &AbsPath) -> anyhow::Result<&AbsPath> {
    let path = dunce::simplified(path.as_ref());
    // This should not fail, but better not panic.
    AbsPath::new(path)
}

pub fn remove_dir<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<()> {
    let _guard = IoCounterKey::RmDir.guard();
    fs::remove_dir(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("remove_dir({})", P::as_ref(&path).display()))
}

pub struct FileWriteGuard {
    file: File,
    _guard: IoCounterGuard,
}

impl Write for FileWriteGuard {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.file.write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.file.flush()
    }
}

pub fn create_file<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<FileWriteGuard> {
    let guard = IoCounterKey::Write.guard();
    let file = File::create(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("create_file({})", P::as_ref(&path).display()))?;
    Ok(FileWriteGuard {
        file,
        _guard: guard,
    })
}

pub struct FileReadGuard {
    file: File,
    _guard: IoCounterGuard,
}

impl Read for FileReadGuard {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.file.read(buf)
    }
}

pub fn open_file<P: AsRef<AbsPath>>(path: P) -> anyhow::Result<FileReadGuard> {
    let guard = IoCounterKey::Read.guard();
    let file = File::open(path.as_ref().as_maybe_relativized())
        .with_context(|| format!("open_file({})", P::as_ref(&path).display()))?;
    Ok(FileReadGuard {
        file,
        _guard: guard,
    })
}

// Create a relative path in a cross-patform way, we need this since RelativePath fails when
// converting backslashes which means windows paths end up failing. RelativePathBuf doesn't have
// this problem and we can easily coerce it into a RelativePath.
// TODO(T143971518) Avoid RelativePath usage in buck2
pub fn relative_path_from_system(path: &Path) -> anyhow::Result<Cow<'_, RelativePath>> {
    let res = if cfg!(windows) {
        Cow::Owned(RelativePathBuf::from_path(path)?)
    } else {
        Cow::Borrowed(RelativePath::from_path(path)?)
    };
    Ok(res)
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::fs::File;
    use std::io;
    use std::path::Path;
    use std::path::PathBuf;

    use assert_matches::assert_matches;
    use relative_path::RelativePath;

    use crate::fs::fs_util;
    use crate::fs::fs_util::create_dir_all;
    use crate::fs::fs_util::metadata;
    use crate::fs::fs_util::read_dir_if_exists;
    use crate::fs::fs_util::read_to_string;
    use crate::fs::fs_util::remove_all;
    use crate::fs::fs_util::remove_dir_all;
    use crate::fs::fs_util::remove_file;
    use crate::fs::fs_util::symlink;
    use crate::fs::fs_util::symlink_metadata;
    use crate::fs::fs_util::write;
    use crate::fs::paths::abs_norm_path::AbsNormPath;
    use crate::fs::paths::abs_path::AbsPath;
    use crate::fs::paths::forward_rel_path::ForwardRelativePath;

    #[test]
    fn if_exists_read_dir() -> anyhow::Result<()> {
        let binding = std::env::temp_dir();
        let existing_path = AbsNormPath::new(&binding)?;
        let res = read_dir_if_exists(existing_path)?;
        assert!(res.is_some());
        let not_existing_dir = existing_path.join(ForwardRelativePath::unchecked_new("dir"));
        let res = read_dir_if_exists(not_existing_dir)?;
        assert!(res.is_none());
        Ok(())
    }

    #[test]
    fn create_and_remove_symlink_dir() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = tempdir.path().join("root");
        let root = AbsPath::new(&root)?;
        create_dir_all(&root)?;
        let dir1 = root.join("dir1");
        let symlink_dir1 = root.join("symlink_dir1");

        // Create dir1 and link symlink_dir1 to dir1
        create_dir_all(&dir1)?;
        assert!(symlink_metadata(&dir1)?.is_dir());
        symlink(&dir1, &symlink_dir1)?;
        assert!(symlink_metadata(&symlink_dir1)?.is_symlink());

        // Remove the symlink, dir1 should still be in tact
        remove_file(&symlink_dir1)?;
        assert!(symlink_metadata(&dir1)?.is_dir());
        assert_matches!(symlink_metadata(&symlink_dir1), Err(..));

        // Clean up
        remove_dir_all(&root)?;
        Ok(())
    }

    #[test]
    fn create_and_remove_symlink_file() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = tempdir.path().join("root");
        let root = AbsPath::new(&root)?;
        create_dir_all(&root)?;
        let file1 = root.join("file1");
        let symlink_file1 = root.join("symlink_file1");

        // Create file1 and link symlink_file1 to file1
        File::create(&file1)?;
        assert!(symlink_metadata(&file1)?.is_file());
        symlink(&file1, &symlink_file1)?;
        assert!(symlink_metadata(&symlink_file1)?.is_symlink());

        // Remove the symlink, file1 should still be in tact
        remove_file(&symlink_file1)?;
        assert!(symlink_metadata(&file1)?.is_file());
        assert_matches!(symlink_metadata(&symlink_file1), Err(..));

        // Clean up
        remove_dir_all(root)?;
        Ok(())
    }

    #[test]
    fn test_symlink_with_target_length_over_max_path() -> anyhow::Result<()> {
        // In Windows, the maximum length of a path is 260.
        // To allow extended path lengths, canonicalize the paths
        // so that they are prefixed with '\\?'
        let max_path = 260;

        // 1. Test a case where we create a simple simlink to a long file path
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let symlink1 = root.join("symlink1");

        // Create a path that looks like <tmp>/subdir/subdir/.../subdir/simple_file
        let mut long_sub_path = "subdir/".repeat(max_path / 7);
        long_sub_path.push_str("simple_file");
        let target_path1 = root.join(long_sub_path);
        assert!(target_path1.to_str().unwrap().len() > max_path);

        create_dir_all(target_path1.parent().unwrap())?;
        symlink(&target_path1, &symlink1)?;
        write(&target_path1, b"This is File1")?;
        assert_eq!(read_to_string(&symlink1)?, "This is File1");

        // 2. Test a case where we create a symlink to an absolute path target with some relative ../../
        let symlink2 = root.join("symlink2");

        // Create a path that looks like <tmp>/subdir/subdir/.../subdir/../abs_with_relative
        let long_sub_path = "subdir/".repeat(max_path / 7);
        let target_path2 = root.join(long_sub_path).join("../abs_with_relative");
        assert!(target_path2.is_absolute());
        assert!(target_path2.to_str().unwrap().len() > max_path);
        create_dir_all(&target_path2)?;
        let target_path2 = target_path2.join("file2");
        symlink(&target_path2, &symlink2)?;
        write(&target_path2, b"This is File2")?;
        assert_eq!(read_to_string(&symlink2)?, "This is File2");
        Ok(())
    }

    #[test]
    fn symlink_to_file_which_doesnt_exist() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let symlink_path = root.join("symlink");
        let target_path = root.join("file");
        symlink(&target_path, &symlink_path)?;
        write(&target_path, b"File content")?;
        assert_eq!(read_to_string(&symlink_path)?, "File content");
        Ok(())
    }

    #[test]
    fn symlink_to_symlinked_dir() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let dir_path = root.join("dir");
        let file_path = dir_path.join("file");
        create_dir_all(&dir_path)?;
        write(file_path, b"Content")?;
        let symlink1_path = dir_path.join("symlink1");
        let symlink2_path = dir_path.join("symlink2");
        symlink(&dir_path, &symlink1_path)?;
        symlink(&symlink1_path, &symlink2_path)?;
        assert_eq!(read_to_string(symlink2_path.join("file"))?, "Content");
        assert!(metadata(&symlink1_path)?.is_dir());
        assert!(metadata(symlink2_path)?.is_dir());
        Ok(())
    }

    #[test]
    fn relative_symlink_to_nonexistent_file() -> anyhow::Result<()> {
        // tmp -- dir1 (exists) -- file1 (doesn't exist)
        //     \
        //      \ symlink1 to dir1/file1
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let dir_path = root.join("dir1");
        create_dir_all(dir_path)?;
        let relative_symlink1_path = root.join("relative_symlink1");
        symlink("dir1/file1", &relative_symlink1_path)?;
        write(root.join("dir1/file1"), b"File content")?;
        assert_eq!(read_to_string(&relative_symlink1_path)?, "File content");
        Ok(())
    }

    #[test]
    fn relative_symlink_to_nonexistent_dir() -> anyhow::Result<()> {
        // tmp -- dir1 (doesn't exists) -- file1 (doesn't exist)
        //     \
        //      \ dir2 -- relative_symlink1 to ../dir1/file1
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let dir1 = root.join("dir1");
        let dir2 = root.join("dir2");
        // Only create dir2 for the symlink creation
        create_dir_all(&dir2)?;
        let relative_symlink1_path = dir2.join("relative_symlink1");
        // Symlink creation should still work even if dir1 doesn't exist yet
        symlink("../dir1/file1", &relative_symlink1_path)?;
        // Create dir1, test that we can write into file1 and symlink1
        create_dir_all(dir1)?;
        write(root.join("dir1/file1"), b"File content")?;
        assert_eq!(read_to_string(&relative_symlink1_path)?, "File content");
        write(&relative_symlink1_path, b"File content 2")?;
        assert_eq!(read_to_string(root.join("dir1/file1"))?, "File content 2");
        Ok(())
    }

    #[test]
    fn relative_symlink_from_symlinked_dir_windows() -> anyhow::Result<()> {
        use crate::fs::fs_util::read_link;

        if !cfg!(windows) {
            return Ok(());
        }

        // tmp1 -- dir1 -- subdir1 -- file1
        // tmp2 -- symlink_to_subdir1 (points to /tmp1/dir1/subdir1) -- (file1)             (points to /tmp1/dir1/subdir1/file1)
        //     \                                                    \-- symlink_to_file2    (want to symlink to ../dir2/file2, which is actually /tmp2/dir2/file2)
        //      \                                                   \-- symlink_to_file3    (want to symlink to ../dir2/file3, which is actually /tmp2/dir2/file3)
        //       \                                                  \-- symlink_to_symlink1 (want to symlink to file1, which is actually /tmp/2/dir1/subdir1/file1)
        //        \ dir2 -- file2
        //              \-- file3 (doesn't exist yet)

        // Construct the directory structure
        let tempdir1 = tempfile::tempdir()?;
        let tempdir2 = tempfile::tempdir()?;
        let root1 = AbsPath::new(tempdir1.path())?;
        let root2 = AbsPath::new(tempdir2.path())?;
        let dir1 = root1.join("dir1");
        let subdir1 = dir1.join("subdir1");
        let file1 = subdir1.join("file1");
        let dir2 = root2.join("dir2");
        let file2 = dir2.join("file2");
        let file3 = dir2.join("file3");
        create_dir_all(&subdir1)?;
        create_dir_all(&dir2)?;
        write(&file1, b"File content 1")?;
        write(&file2, b"File content 2")?;

        // Simple symlink to a directory
        let symlink_to_subdir1 = root2.join("symlink_to_subdir1");
        symlink(&subdir1, &symlink_to_subdir1)?;
        assert_eq!(read_link(&symlink_to_subdir1)?, subdir1.as_path());
        assert_eq!(
            read_to_string(symlink_to_subdir1.join("file1"))?,
            "File content 1"
        );

        // Test1: A relative symlink that needs to get converted to the realpath correctly
        // /tmp2/symlink_to_subdir1/symlink_to_file2 would live in /tmp1/dir1/subdir1/file2, which means the relative symlink is incorrect
        // Test that symlink properly converts to canonicalized target path
        let symlink_to_file2 = symlink_to_subdir1.join("symlink_to_file2");
        symlink("../dir2/file2", &symlink_to_file2)?;
        assert_eq!(read_link(&symlink_to_file2)?, file2.as_path());
        assert_eq!(read_to_string(&symlink_to_file2)?, "File content 2");

        // Test2: Same case as test1, but target file doesn't exist yet
        let symlink_to_file3 = symlink_to_subdir1.join("symlink_to_file3");
        symlink("../dir2/file3", &symlink_to_file3)?;
        write(&file3, b"File content 3")?;
        assert_eq!(read_link(&symlink_to_file3)?, file3.as_path());
        assert_eq!(read_to_string(&file3)?, "File content 3");
        assert_eq!(read_to_string(&symlink_to_file3)?, "File content 3");

        // Test3: Create a symlink from a symlinked directory to another symlink in the same directory
        let symlink_to_symlink1 = symlink_to_subdir1.join("symlink_to_symlink1");
        symlink("../symlink_to_subdir1/file1", &symlink_to_symlink1)?;
        assert_eq!(read_link(&symlink_to_symlink1)?, file1.as_path());
        assert_eq!(read_to_string(&symlink_to_symlink1)?, "File content 1");
        Ok(())
    }

    #[test]
    fn absolute_symlink_to_nonexistent_file_in_nonexistent_dir() -> anyhow::Result<()> {
        // tmp -- dir1 (doesn't exists) -- file1 (doesn't exist)
        //     \
        //      \ symlink1 to /tmp/dir1/file1
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let abs_symlink1_path = root.join("abs_symlink1");
        let target_abs_path = root.join("dir1/file1");
        symlink(&target_abs_path, &abs_symlink1_path)?;
        create_dir_all(root.join("dir1"))?;
        write(&target_abs_path, b"File content")?;
        assert_eq!(read_to_string(&abs_symlink1_path)?, "File content");
        Ok(())
    }

    #[test]
    fn remove_file_removes_symlink_to_directory() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let symlink_path = root.join("symlink_dir");
        let dir_path = root.join("dir");
        let file_path = dir_path.join("file");
        create_dir_all(&dir_path)?;
        write(file_path, b"File content")?;
        symlink(&dir_path, &symlink_path)?;
        let symlinked_path = symlink_path.join("file");
        assert_eq!(read_to_string(symlinked_path)?, "File content");
        remove_file(&symlink_path)?;
        assert_eq!(
            io::ErrorKind::NotFound,
            fs::metadata(&symlink_path).unwrap_err().kind()
        );
        Ok(())
    }

    #[test]
    fn remove_file_does_not_remove_directory() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let dir_path = root.join("dir");
        create_dir_all(AbsPath::new(&dir_path)?)?;
        assert_matches!(remove_file(&dir_path), Err(..));
        assert!(fs::try_exists(&dir_path)?);
        Ok(())
    }

    #[test]
    fn remove_file_broken_symlink() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let symlink_path = tempdir.path().join("symlink");
        let symlink_path = AbsPath::new(&symlink_path)?;
        symlink("path_which_doesnt_exist", symlink_path)?;
        remove_file(symlink_path)?;
        assert_eq!(
            io::ErrorKind::NotFound,
            fs::symlink_metadata(symlink_path).unwrap_err().kind()
        );
        Ok(())
    }

    #[test]
    fn remove_file_non_existing_file() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let file_path = root.join("file_doesnt_exist");
        assert_matches!(remove_file(file_path), Err(..));
        Ok(())
    }

    #[test]
    fn remove_all_nonexistent() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        remove_all(root.join("nonexistent"))?;
        Ok(())
    }

    #[test]
    fn remove_all_regular() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let path = root.join("file");
        fs::write(&path, b"regular")?;
        remove_all(&path)?;
        assert!(!fs::try_exists(&path)?);
        Ok(())
    }

    #[test]
    fn remove_all_dir() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let path = root.join("dir");
        fs::create_dir(&path)?;
        fs::write(path.join("file"), b"regular file in a dir")?;
        remove_all(&path)?;
        assert!(!fs::try_exists(&path)?);
        Ok(())
    }

    #[test]
    fn remove_all_broken_symlink() -> anyhow::Result<()> {
        fn ls(path: &Path) -> anyhow::Result<Vec<PathBuf>> {
            let mut entries = fs::read_dir(path)?
                .map(|entry| Ok(entry.map(|entry| entry.path())?))
                .collect::<anyhow::Result<Vec<_>>>()?;
            entries.sort();
            Ok(entries)
        }

        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let target = root.join("non-existent-target");
        let path = root.join("symlink");
        symlink(target, &path)?;

        assert_eq!(vec![path.as_path()], ls(tempdir.path())?, "Sanity check");

        remove_all(&path)?;

        // We cannot use `exists` here because it does not report what we need on broken symlink.
        assert_eq!(Vec::<PathBuf>::new(), ls(tempdir.path())?);

        Ok(())
    }

    #[test]
    fn remove_dir_all_does_not_remove_file() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let file_path = root.join("file");
        fs::write(&file_path, b"File content")?;
        assert!(remove_dir_all(&file_path).is_err());
        assert!(fs::try_exists(&file_path)?);
        Ok(())
    }

    #[test]
    fn create_dir_if_not_exists() {
        let tempdir = tempfile::tempdir().unwrap();
        let tempdir = AbsPath::new(tempdir.path()).unwrap();
        fs_util::create_dir_if_not_exists(tempdir.join("dir1")).unwrap();
        assert!(
            fs_util::symlink_metadata(tempdir.join("dir1"))
                .unwrap()
                .is_dir()
        );
        fs_util::create_dir_if_not_exists(tempdir.join("dir1")).unwrap();
        assert!(
            fs_util::symlink_metadata(tempdir.join("dir1"))
                .unwrap()
                .is_dir()
        );

        assert!(fs_util::create_dir_if_not_exists(tempdir.join("dir2/file")).is_err());
        assert!(!fs_util::try_exists(tempdir.join("dir2")).unwrap());

        fs_util::write(tempdir.join("file"), b"rrr").unwrap();
        assert!(fs_util::create_dir_if_not_exists(tempdir.join("file")).is_err());
    }

    #[test]
    fn test_read_to_string_opt() -> anyhow::Result<()> {
        let tempdir = tempfile::tempdir()?;
        let root = AbsPath::new(tempdir.path())?;
        let f1 = root.join("f1");
        let f2 = root.join("f2");

        fs_util::write(&f1, b"data")?;
        assert_eq!(fs_util::read_to_string_opt(&f1)?.as_deref(), Some("data"));
        assert_eq!(fs_util::read_to_string_opt(f2)?, None);

        Ok(())
    }

    #[cfg(windows)]
    #[test]
    fn test_windows_relative_path() -> anyhow::Result<()> {
        assert_eq!(
            fs_util::relative_path_from_system(Path::new("foo\\bar"))?,
            RelativePath::new("foo/bar")
        );
        Ok(())
    }

    #[cfg(unix)]
    #[test]
    fn test_relative_path() -> anyhow::Result<()> {
        assert_eq!(
            fs_util::relative_path_from_system(Path::new("foo/bar"))?,
            RelativePath::new("foo/bar")
        );
        Ok(())
    }

    #[test]
    fn test_set_executable() {
        let tempdir = tempfile::tempdir().unwrap();
        let root = AbsPath::new(tempdir.path()).unwrap();
        let path = root.join("file");
        fs_util::write(&path, b"rrr").unwrap();
        fs_util::set_executable(&path).unwrap();

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;

            let mode = fs_util::metadata(&path).unwrap().permissions().mode();
            assert_eq!(0o111, mode & 0o111);
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

pub mod artifact_path_resolver;
pub mod async_fs_util;
pub mod buck_out_path;
pub mod cwd;
pub mod fs_util;
pub mod paths;
pub mod project;
pub mod project_rel_path;
pub mod working_dir;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs::File;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::Context;
use dupe::Dupe;
use ref_cast::RefCast;
use relative_path::RelativePathBuf;

use crate::fs::fs_util;
use crate::fs::paths::abs_norm_path::AbsNormPath;
use crate::fs::paths::abs_norm_path::AbsNormPathBuf;
use crate::fs::paths::forward_rel_path::ForwardRelativePath;
use crate::fs::paths::RelativePath;

#[derive(Debug, thiserror::Error)]
enum ProjectRootError {
    #[error("Provided project root `{0}` is not equal to the canonicalized path `{1}`")]
    NotCanonical(AbsNormPathBuf, AbsNormPathBuf),
    #[error("Project root `{0}` not found in path `{1}`")]
    ProjectRootNotFound(ProjectRoot, AbsPathBuf),
}

/// The 'ProjectFilesystem' that contains the root path and the current working
/// directory (cwd). The root path is the project root as defined in this
/// library. The cwd will be the directory from which the command was invoked,
/// which is within the project root and hence relativized against it.
#[derive(Clone, Debug, Dupe, PartialEq, derive_more::Display, Allocative)]
#[display(fmt = "{root}")]
pub struct ProjectRoot {
    root: Arc<AbsNormPathBuf>,
}

pub struct ProjectRootTemp {
    path: ProjectRoot,
    // Important field as we want to keep this alive while the path is in use
    _temp: tempfile::TempDir,
}

impl ProjectRootTemp {
    /// creates a filesystem at a temporary root where the cwd is set to the
    /// same root
    pub fn new() -> anyhow::Result<Self> {
        let temp = tempfile::tempdir()?;
        let path = fs_util::canonicalize(temp.path())?;
        let path = ProjectRoot::new(path)?;
        Ok(Self { path, _temp: temp })
    }

    pub fn path(&self) -> &ProjectRoot {
        &self.path
    }

    pub fn write_file(&self, path: &str, content: &str) {
        let path = ProjectRelativePath::new(path).unwrap();
        self.path().write_file(path, content, false).unwrap();
    }
}

impl ProjectRoot {
    pub fn new(root: AbsNormPathBuf) -> anyhow::Result<Self> {
        let canon = fs_util::canonicalize(&root).context("canonicalize project root")?;
        if canon != root {
            return Err(ProjectRootError::NotCanonical(root, canon).into());
        }
        Ok(ProjectRoot {
            // We store the canonicalized path here because even if path
            // is equal to the canonicalized path, it may differ in the slashes or the case.
            root: Arc::new(canon),
        })
    }

    pub fn new_unchecked(root: AbsNormPathBuf) -> ProjectRoot {
        ProjectRoot {
            root: Arc::new(root),
        }
    }

    pub fn root(&self) -> &AbsNormPath {
        &self.root
    }

    pub fn name(&self) -> Option<&str> {
        self.root.file_name().and_then(OsStr::to_str)
    }

    ///
    /// Takes a 'ProjectRelativePath' and resolves it against the current
    /// `project root`, yielding a 'AbsPathBuf'
    ///
    /// ```
    /// use buck2_core::fs::project::ProjectRoot;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    /// use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
    ///
    /// if cfg!(not(windows)) {
    ///     let root = AbsNormPathBuf::from("/usr/local/fbsource/".into())?;
    ///     let fs = ProjectRoot::new_unchecked(root);
    ///
    ///     assert_eq!(
    ///         AbsNormPathBuf::from("/usr/local/fbsource/buck/BUCK".into())?,
    ///         fs.resolve(ProjectRelativePath::new("buck/BUCK")?)
    ///     );
    /// } else {
    ///     let root = AbsNormPathBuf::from("c:/open/fbsource/".into())?;
    ///     let fs = ProjectRoot::new_unchecked(root);
    ///
    ///     assert_eq!(
    ///         AbsNormPathBuf::from("c:/open/fbsource/buck/BUCK".into())?,
    ///         fs.resolve(ProjectRelativePath::new("buck/BUCK")?)
    ///     );
    /// }
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn resolve(&self, path: impl PathLike) -> AbsNormPathBuf {
        path.resolve(self).into_owned()
    }

    ///
    /// Takes a 'ProjectRelativePath' and converts it to a 'Path' that is relative to the project root.
    ///
    /// ```
    /// use buck2_core::fs::project::{ProjectRoot};
    /// use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
    /// use std::path::PathBuf;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// let root = if cfg!(not(windows)) {
    ///     AbsNormPathBuf::from("/usr/local/fbsource/".into())?
    /// } else {
    ///     AbsNormPathBuf::from("c:/open/fbsource/".into())?
    /// };
    /// let fs = ProjectRoot::new_unchecked(root);
    ///
    /// assert_eq!(
    ///     PathBuf::from("buck/BUCK"),
    ///     fs.as_relative_path(ProjectRelativePath::new("buck/BUCK")?)
    /// );
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn as_relative_path<P: AsRef<ProjectRelativePath>>(&self, path: P) -> PathBuf {
        let rel: &RelativePath = (path.as_ref().0).as_ref();
        PathBuf::from(rel.as_str())
    }

    ///
    /// Given an 'AbsPath', attempts to relativize the 'AbsPath' against the
    /// `project root` by stripping the prefix of the given paths.
    ///
    /// Errors if the given path is not a sub directory of the root.
    ///
    /// ```
    /// use std::borrow::Cow;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    /// use buck2_core::fs::paths::abs_norm_path::{AbsNormPathBuf, AbsNormPath};
    /// use buck2_core::fs::project::ProjectRoot;
    ///
    /// if cfg!(not(windows)) {
    ///     let root = AbsNormPathBuf::from("/usr/local/fbsource/".into())?;
    ///     let fs = ProjectRoot::new_unchecked(root);
    ///
    ///     assert_eq!(
    ///         Cow::Borrowed(ProjectRelativePath::new("src/buck.java")?),
    ///         fs.relativize(AbsNormPath::new("/usr/local/fbsource/src/buck.java")?)?
    ///     );
    ///     assert!(fs.relativize(AbsNormPath::new("/other/path")?).is_err());
    /// } else {
    ///     let root = AbsNormPathBuf::from("c:/open/fbsource/".into())?;
    ///     let fs = ProjectRoot::new_unchecked(root);
    ///
    ///     assert_eq!(
    ///         Cow::Borrowed(ProjectRelativePath::new("src/buck.java")?),
    ///         fs.relativize(AbsNormPath::new("c:/open/fbsource/src/buck.java")?)?
    ///     );
    ///     assert_eq!(
    ///         Cow::Borrowed(ProjectRelativePath::new("src/buck.java")?),
    ///         fs.relativize(AbsNormPath::new(r"C:\open\fbsource\src\buck.java")?)?
    ///     );
    ///     assert_eq!(
    ///         Cow::Borrowed(ProjectRelativePath::new("src/buck.java")?),
    ///         fs.relativize(AbsNormPath::new(r"\\?\C:\open\fbsource\src\buck.java")?)?
    ///     );
    ///     assert!(fs.relativize(AbsNormPath::new("c:/other/path")?).is_err());
    /// }
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn relativize<'a, P: ?Sized + AsRef<AbsNormPath>>(
        &self,
        p: &'a P,
    ) -> anyhow::Result<Cow<'a, ProjectRelativePath>> {
        let relative_path = p.as_ref().strip_prefix(self.root()).map_err(|_| {
            anyhow::anyhow!(
                "Error relativizing: `{}` is not relative to project root `{}`",
                p.as_ref(),
                self.root()
            )
        })?;
        match relative_path {
            Cow::Borrowed(p) => Ok(Cow::Borrowed(ProjectRelativePath::ref_cast(p))),
            Cow::Owned(p) => Ok(Cow::Owned(ProjectRelativePathBuf::from(p))),
        }
    }

    /// Remove project root prefix from path (even if path is not canonical)
    /// and return the remaining path.
    ///
    /// Fail if canonicalized path does not start with project root.
    fn strip_project_root<'a>(&'a self, path: &'a AbsPath) -> anyhow::Result<PathBuf> {
        let path = fs_util::simplified(path)?;

        if let Ok(rem) = Path::strip_prefix(path, &*self.root) {
            // Fast code path.
            return Ok(rem.to_path_buf());
        }

        // Now try to canonicalize the path. We cannot call `canonicalize` on the full path
        // because we should only resolve symlinks found in the past that point into the project,
        // but
        // * not symlink found inside the project that point outside of it
        // * not even symlinks found in the project unless we need to to resolve ".."

        let mut current_prefix = PathBuf::new();

        let mut components = path.components();
        while let Some(comp) = components.next() {
            current_prefix.push(comp);

            // This is not very efficient, but efficient cross-platform implementation is not easy.
            let canonicalized_current_prefix = fs_util::canonicalize(&current_prefix)?;

            if let Ok(rem) = canonicalized_current_prefix
                .as_path()
                .strip_prefix(self.root.as_path())
            {
                // We found the project root.
                return Ok(rem.join(components.as_path()));
            }
        }

        Err(ProjectRootError::ProjectRootNotFound(self.dupe(), path.to_owned()).into())
    }

    fn relativize_any_impl(&self, path: &AbsPath) -> anyhow::Result<ProjectRelativePathBuf> {
        let project_relative = self.strip_project_root(path)?;
        // TODO(nga): this does not treat `..` correctly.
        //   See the test below for an example.
        // This must use `RelativePathBuf`, not `RelativePath`,
        // because `RelativePathBuf` handles backslashes on Windows, and `RelativePath` does not.
        ProjectRelativePath::empty().join_normalized(RelativePathBuf::from_path(project_relative)?)
    }

    /// Relativize an absolute path which may be not normalized or not canonicalize.
    /// This operation may involve disk access.
    pub fn relativize_any<P: AsRef<AbsPath>>(
        &self,
        path: P,
    ) -> anyhow::Result<ProjectRelativePathBuf> {
        let path = path.as_ref();
        self.relativize_any_impl(path.as_ref()).with_context(|| {
            format!(
                "relativize path `{}` against project root `{}`",
                path.display(),
                self
            )
        })
    }

    // TODO(nga): refactor this to global function.
    pub fn write_file(
        &self,
        path: impl PathLike,
        contents: impl AsRef<[u8]>,
        executable: bool,
    ) -> anyhow::Result<()> {
        let abs_path = path.resolve(self);
        if let Some(parent) = abs_path.parent() {
            fs_util::create_dir_all(parent).with_context(|| {
                format!(
                    "`write_file` for `{}` creating directory `{}`",
                    abs_path.as_ref(),
                    parent
                )
            })?;
        }
        fs_util::write(abs_path.as_ref(), contents)
            .with_context(|| format!("`write_file` writing `{}`", abs_path.as_ref()))?;
        if executable {
            self.set_executable(abs_path.as_ref()).with_context(|| {
                format!("`write_file` setting executable `{}`", abs_path.as_ref())
            })?;
        }
        Ok(())
    }

    // TODO(nga): refactor this to global function.
    pub fn create_file(&self, path: impl PathLike, executable: bool) -> anyhow::Result<File> {
        let abs_path = path.resolve(self);
        if let Some(parent) = abs_path.parent() {
            fs_util::create_dir_all(parent).with_context(|| {
                format!(
                    "`create_file` for `{}` creating directory `{}`",
                    abs_path.as_ref(),
                    parent
                )
            })?;
        }
        let file = File::create(abs_path.as_ref())
            .with_context(|| format!("`create_file` creating `{}`", abs_path.as_ref()))?;
        if executable {
            self.set_executable(abs_path.as_ref()).with_context(|| {
                format!("`create_file` setting executable `{}`", abs_path.as_ref())
            })?;
        }
        Ok(file)
    }

    // TODO(nga): refactor this to global function.
    pub fn set_executable(&self, path: impl PathLike) -> anyhow::Result<()> {
        let path = path.resolve(self);
        fs_util::set_executable(path.as_ref())
    }

    /// Create a soft link from one location to another.
    ///
    /// There is no requirement that `src` must exist,
    /// and `src` can be either an absolute or relative path.
    ///
    /// This function is "raw" in the sense that it passes the `src`
    /// directly to underlying fs function calls. We do not verify
    /// anything about the incoming path. Other functions generally
    /// require things like "is a project relative path", etc.
    ///
    /// Filesystems that do not support soft links will return `Err`.
    // TODO(nga): refactor this to global function.
    pub fn soft_link_raw(&self, src: impl AsRef<Path>, dest: impl PathLike) -> anyhow::Result<()> {
        let dest_abs = self.resolve(dest);

        if let Some(parent) = dest_abs.parent() {
            fs_util::create_dir_all(parent)?;
        }
        fs_util::symlink(src, dest_abs)
    }

    /// Create a relative symlink between two relative paths
    ///
    /// This changes the path that `dest` is linked to, to the
    /// relative path to get to `src` from `dest`. Useful when
    /// one wants to link together, e.g. to `ProjectRelativePath`s
    ///
    /// e.g. given a `src` of `foo/bar1/baz1/out` and `dest` of `foo/bar2/baz2/out`,
    ///      `readlink` on `dest` would yield `../../bar1/baz1/out`
    ///
    /// `src`: Relative path that does not need to exist
    /// `dest`: Relative path that will be linked to `src`
    ///         using the relative traversal between the two
    ///
    /// Errors if the link could not be created (generally due to FS support of symlinks)
    // TODO(nga): refactor this to global function.
    pub fn soft_link_relativized(
        &self,
        src: impl PathLike,
        dest: impl PathLike,
    ) -> anyhow::Result<()> {
        let target_abs = self.resolve(src);
        let dest_abs = self.resolve(dest);

        let target_relative = Self::find_relative_path(&target_abs, &dest_abs);
        if let Some(parent) = dest_abs.parent() {
            fs_util::create_dir_all(parent)?;
        }
        fs_util::symlink(target_relative, dest_abs)
    }

    /// Copy from one path to another. This works for both files and directories.
    ///
    /// This copy works by:
    ///  - Copying directories recursively
    ///  - Re-writing relative symlinks. That is, a link to `foo/bar` might end up
    ///    as `../../../other/foo/bar` in the destination. Absolute symlinks are not changed.
    // TODO(nga): refactor this to global function.
    pub fn copy(&self, src: impl PathLike, dest: impl PathLike) -> anyhow::Result<()> {
        let src_abs = self.resolve(src);
        let dest_abs = self.resolve(dest);

        let result = self.copy_resolved(&src_abs, &dest_abs);
        result.with_context(|| {
            format!(
                "Error copying from src path `{}` to dest path `{}`",
                src_abs, dest_abs
            )
        })
    }

    fn copy_resolved(
        &self,
        src_abs: &AbsNormPathBuf,
        dest_abs: &AbsNormPathBuf,
    ) -> anyhow::Result<()> {
        let src_type = fs_util::symlink_metadata(src_abs)?.file_type();

        if let Some(parent) = dest_abs.parent() {
            fs_util::create_dir_all(parent)?;
        }
        if src_type.is_dir() {
            Self::copy_dir(src_abs, dest_abs)
        } else if src_type.is_file() {
            Self::copy_file(src_abs, dest_abs)
        } else if src_type.is_symlink() {
            Self::copy_symlink(src_abs, dest_abs)
        } else {
            // If we want to handle special files, we'll need to use special traits
            // https://doc.rust-lang.org/std/os/unix/fs/trait.FileTypeExt.html
            Err(anyhow::anyhow!(
                "Attempted to copy a path ({}) of an unknown type",
                src_abs
            ))
        }
    }

    /// Remove a path recursively, regardless of it being a file or a directory (all contents
    /// deleted).
    /// This does not follow symlinks, and only removes the link itself.
    // TODO(nga): refactor this to global function.
    pub fn remove_path_recursive(&self, path: impl PathLike) -> anyhow::Result<()> {
        let path = self.resolve(path);

        // TODO: This should probably not use symlink_metadata_if_available... But it used to use
        // Path::exists()...
        let meta = match fs_util::symlink_metadata_if_available(&path) {
            Some(m) => m,
            None => return Ok(()),
        };

        let path_type = meta.file_type();

        if path_type.is_dir() {
            fs_util::remove_dir_all(&path)
                .with_context(|| format!("remove_path_recursive({}) on directory", &path))?;
        } else if path_type.is_file() || path_type.is_symlink() {
            fs_util::remove_file(&path)
                .with_context(|| format!("remove_path_recursive({}) on file", &path))?;
        } else {
            // If we want to handle special files, we'll need to use special traits
            // https://doc.rust-lang.org/std/os/unix/fs/trait.FileTypeExt.html
            return Err(anyhow::anyhow!(
                "remove_path_recursive, attempted to delete a path ({}) of an unknown type",
                path
            ));
        }

        Ok(())
    }

    /// Find the relative path between two paths within the project
    pub fn relative_path(&self, target: impl PathLike, dest: impl PathLike) -> PathBuf {
        Self::find_relative_path(&self.resolve(target), &self.resolve(dest))
    }

    /// Find the relative path between two absolute ones
    ///
    /// Given two absolute paths, get the relative path from `dest` to `target`.
    ///
    /// e.g. given a `target` of `/foo/bar1/baz1/out` and `dest` of `/foo/bar2/baz2/out`, the
    ///      result would be `../../bar1/baz1/out`
    fn find_relative_path(target: &AbsNormPathBuf, dest: &AbsNormPathBuf) -> PathBuf {
        use itertools::EitherOrBoth::*;
        use itertools::Itertools;
        // Assemble both the '../' traversal, and the component that will come after that
        let mut upward_traversal = PathBuf::new();
        let mut relative_to_common_path = PathBuf::new();
        // So that /foo/bar/quz and /baz/bar/quz don't look like the same path
        // in the second component
        let mut diverged = false;

        for component in target
            .iter()
            .zip_longest(dest.parent().expect("a path with a parent in dest").iter())
        {
            match component {
                Both(t, d) => {
                    if diverged || t != d {
                        diverged = true;
                        upward_traversal.push(Component::ParentDir);
                        relative_to_common_path.push(t);
                    }
                }
                Left(t) => {
                    diverged = true;
                    relative_to_common_path.push(t)
                }
                Right(_) => {
                    diverged = true;
                    upward_traversal.push(Component::ParentDir)
                }
            }
        }
        upward_traversal.push(relative_to_common_path);
        upward_traversal
    }

    /// Creates symbolic link `dest` which points at the same location as symlink `src`.
    fn copy_symlink(src: &AbsNormPathBuf, dest: &AbsNormPathBuf) -> anyhow::Result<()> {
        let mut target = fs_util::read_link(src)?;
        if target.is_relative() {
            // Grab the absolute path, then re-relativize the path to the destination
            let relative_target = fs_util::relative_path_from_system(target.as_path())?;
            let absolute_target = relative_target.normalize().to_path(
                src.parent()
                    .expect("a path with a parent in symlink target"),
            );
            target = Self::find_relative_path(&AbsNormPathBuf::try_from(absolute_target)?, dest);
        }
        fs_util::symlink(target, dest)
    }

    fn copy_file(src: &AbsNormPathBuf, dst: &AbsNormPathBuf) -> anyhow::Result<()> {
        fs_util::copy(src, dst).map(|_| ())
    }

    fn copy_dir(src_dir: &AbsNormPathBuf, dest_dir: &AbsNormPathBuf) -> anyhow::Result<()> {
        fs_util::create_dir_all(dest_dir)?;
        for file in fs_util::read_dir(src_dir)? {
            let file = file?;
            let filetype = file.file_type()?;
            let src_file = file.path();
            let dest_file = dest_dir.join(ForwardRelativePath::new(&file.file_name())?);
            if filetype.is_dir() {
                Self::copy_dir(&src_file, &dest_file)?;
            } else if filetype.is_symlink() {
                Self::copy_symlink(&src_file, &dest_file)?;
            } else if filetype.is_file() {
                Self::copy_file(&src_file, &dest_file)?;
            }
        }
        Ok(())
    }
}

use allocative::Allocative;
pub use internals::PathLike;

use crate::fs::paths::abs_path::AbsPath;
use crate::fs::paths::abs_path::AbsPathBuf;
use crate::fs::project_rel_path::ProjectRelativePath;
use crate::fs::project_rel_path::ProjectRelativePathBuf;

mod internals {
    use std::borrow::Cow;

    use crate::fs::paths::abs_norm_path::AbsNormPath;
    use crate::fs::paths::abs_norm_path::AbsNormPathBuf;
    use crate::fs::project::ProjectRoot;
    use crate::fs::project_rel_path::ProjectRelativePath;
    use crate::fs::project_rel_path::ProjectRelativePathBuf;

    pub trait PathLike: PathLikeResolvable {}

    impl<T> PathLike for T where T: PathLikeResolvable {}

    pub trait PathLikeResolvable {
        fn resolve(&self, fs: &ProjectRoot) -> Cow<'_, AbsNormPath>;
    }

    impl PathLikeResolvable for &AbsNormPath {
        fn resolve(&self, _fs: &ProjectRoot) -> Cow<'_, AbsNormPath> {
            Cow::Borrowed(self)
        }
    }

    impl PathLikeResolvable for &AbsNormPathBuf {
        fn resolve(&self, _fs: &ProjectRoot) -> Cow<'_, AbsNormPath> {
            Cow::Borrowed(self)
        }
    }

    impl PathLikeResolvable for &ProjectRelativePath {
        fn resolve(&self, fs: &ProjectRoot) -> Cow<'_, AbsNormPath> {
            Cow::Owned(self.0.resolve(fs.root()))
        }
    }

    impl PathLikeResolvable for &ProjectRelativePathBuf {
        fn resolve(&self, fs: &ProjectRoot) -> Cow<'_, AbsNormPath> {
            Cow::Owned(self.0.resolve(fs.root()))
        }
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;
    use std::path::PathBuf;

    use crate::fs::fs_util;
    use crate::fs::paths::abs_path::AbsPath;
    use crate::fs::paths::forward_rel_path::ForwardRelativePath;
    use crate::fs::project::ProjectRoot;
    use crate::fs::project::ProjectRootTemp;
    use crate::fs::project_rel_path::ProjectRelativePath;

    #[test]
    fn copy_works() -> anyhow::Result<()> {
        let fs = ProjectRootTemp::new()?;
        let dir1 = ProjectRelativePath::new("dir1")?;
        let dir2 = ProjectRelativePath::new("dir1/dir2")?;
        let dir3 = ProjectRelativePath::new("dir1/dir2/dir3")?;
        let link_dir2 = ProjectRelativePath::new("dir1/link_dir2")?;
        let link_dir3 = ProjectRelativePath::new("dir1/link_dir3")?;
        let link_file3 = ProjectRelativePath::new("dir1/link_file3")?;
        let file1 = ProjectRelativePath::new("dir1/file1")?;
        let file2 = ProjectRelativePath::new("dir1/dir2/file2")?;
        let file3 = ProjectRelativePath::new("dir1/dir2/dir3/file3")?;
        let file4 = ProjectRelativePath::new("dir1/dir2/dir3/file4")?;
        let out_dir = ProjectRelativePath::new("out")?;

        fs_util::create_dir_all(fs.path.resolve(dir1))?;
        fs_util::create_dir_all(fs.path.resolve(dir2))?;
        fs_util::create_dir_all(fs.path.resolve(dir3))?;
        fs_util::create_dir_all(fs.path.resolve(out_dir))?;

        fs_util::write(fs.path.resolve(file1), "file1 contents")?;
        fs_util::write(fs.path.resolve(file2), "file2 contents")?;
        fs_util::write(fs.path.resolve(file3), "file3 contents")?;
        fs_util::write(fs.path.resolve(file4), "file4 contents")?;
        // Absolute path
        fs_util::symlink(fs.path.resolve(dir2), fs.path.resolve(link_dir2))?;
        // Relative path
        fs_util::symlink(Path::new("dir2/dir3"), fs.path.resolve(link_dir3))?;
        fs_util::symlink(Path::new("dir2/dir3/file3"), fs.path.resolve(link_file3))?;

        fs.path
            .copy(
                ProjectRelativePath::new("dir1/file1")?,
                ProjectRelativePath::new("out")?,
            )
            .expect_err("should fail because out exists");

        let expected_dir1 = ProjectRelativePath::new("out/dir1")?;
        let expected_dir2 = ProjectRelativePath::new("out/dir1/dir2")?;
        let expected_dir3 = ProjectRelativePath::new("out/dir1/dir2/dir3")?;
        let expected_link_dir2 = ProjectRelativePath::new("out/dir1/link_dir2")?;
        let expected_link_dir3 = ProjectRelativePath::new("out/dir1/link_dir3")?;
        let expected_link_file3 = ProjectRelativePath::new("out/dir1/link_file3")?;
        let expected_file1 = ProjectRelativePath::new("out/dir1/file1")?;
        let expected_file2 = ProjectRelativePath::new("out/dir1/dir2/file2")?;
        let expected_file3 = ProjectRelativePath::new("out/dir1/dir2/dir3/file3")?;
        let expected_file4 = ProjectRelativePath::new("out/other1/file4")?;

        fs.path.copy(dir1, ProjectRelativePath::new("out/dir1")?)?;

        // Ensure copying a file creates any parent dirs properly
        fs.path
            .copy(file4, ProjectRelativePath::new("out/other1/file4")?)?;

        assert!(std::path::Path::is_dir(
            fs.path.resolve(expected_dir1).as_ref()
        ));
        assert!(std::path::Path::is_dir(
            fs.path.resolve(expected_dir2).as_ref()
        ));
        assert!(std::path::Path::is_dir(
            fs.path.resolve(expected_dir3).as_ref()
        ));
        // Absolute link path
        assert_eq!(
            fs.path.resolve(dir2).as_ref() as &Path,
            fs_util::read_link(fs.path.resolve(expected_link_dir2))?.as_path(),
        );
        // Make sure out/dir1/link_dir3 links to the relative path to dir1/dir2/dir3
        let link_dir3_target = fs_util::read_link(fs.path.resolve(expected_link_dir3))?;
        if cfg!(unix) {
            assert_eq!(
                Path::new("../../dir1/dir2/dir3"),
                link_dir3_target.as_path(),
            );
        } else {
            // In Windows we use absolute path
            assert_eq!(fs.path.resolve(dir3).as_path(), link_dir3_target.as_path());
        }

        // Make sure we can read through; that the relative path actually works
        fs_util::write(fs.path.resolve(file3), "file3 new contents")?;
        let link_file3_target = fs_util::read_link(fs.path.resolve(expected_link_file3))?;
        if cfg!(unix) {
            assert_eq!(
                Path::new("../../dir1/dir2/dir3/file3"),
                link_file3_target.as_path(),
            );
        } else {
            // In Windows we use absolute path
            assert_eq!(
                fs.path.resolve(file3).as_path(),
                link_file3_target.as_path()
            );
        }
        assert_eq!(
            "file3 new contents",
            fs_util::read_to_string(fs.path.resolve(expected_link_file3))?
        );

        assert_eq!(
            "file1 contents",
            fs_util::read_to_string(fs.path.resolve(expected_file1))?
        );
        assert_eq!(
            "file2 contents",
            fs_util::read_to_string(fs.path.resolve(expected_file2))?
        );
        // Independent copy; no hard links made (previous behavior)
        assert_eq!(
            "file3 contents",
            fs_util::read_to_string(fs.path.resolve(expected_file3))?
        );
        assert_eq!(
            "file4 contents",
            fs_util::read_to_string(fs.path.resolve(expected_file4))?
        );
        Ok(())
    }

    #[test]
    fn test_copy_symlink() -> anyhow::Result<()> {
        let fs = ProjectRootTemp::new()?;
        let symlink1 = ProjectRelativePath::new("symlink1")?;
        let symlink2 = ProjectRelativePath::new("symlink2")?;
        let file = ProjectRelativePath::new("file")?;
        fs.path.write_file(file, "hello", false)?;
        fs.path.soft_link_raw(fs.path.resolve(file), symlink1)?;
        fs.path.copy(symlink1, symlink2)?;

        assert_eq!("hello", fs_util::read_to_string(fs.path.resolve(symlink1))?);
        assert_eq!("hello", fs_util::read_to_string(fs.path.resolve(symlink2))?);
        Ok(())
    }

    #[test]
    fn test_symlink_relativized() -> anyhow::Result<()> {
        let fs = ProjectRootTemp::new()?;

        let target1 = ProjectRelativePath::new("foo1/bar1/target")?;
        let target2 = ProjectRelativePath::new("foo2/bar")?;
        let file = target2.join(ForwardRelativePath::new("file")?);

        let dest1 = ProjectRelativePath::new("foo1/target-link")?;
        let dest2 = ProjectRelativePath::new("foo1/bar2/target")?;
        let dest3 = ProjectRelativePath::new("foo1-link/bar1/target")?;
        let dest4 = ProjectRelativePath::new("foo2/bar-link")?;
        let dest5 = ProjectRelativePath::new("foo2-link/bar")?;

        fs.path.write_file(target1, "foo1 contents", false)?;
        fs.path.write_file(&file, "foo2 contents", false)?;

        fs.path.soft_link_relativized(target1, dest1)?;
        fs.path.soft_link_relativized(target1, dest2)?;
        fs.path.soft_link_relativized(target1, dest3)?;
        fs.path.soft_link_relativized(target2, dest4)?;
        fs.path.soft_link_relativized(target2, dest5)?;

        fs.path.write_file(target1, "new foo1 contents", false)?;
        fs.path.write_file(&file, "new foo2 contents", false)?;

        let dest1_expected = PathBuf::from("bar1/target");
        let dest2_expected = PathBuf::from("../bar1/target");
        let dest3_expected = PathBuf::from("../../foo1/bar1/target");
        let dest4_expected = PathBuf::from("bar");
        let dest5_expected = PathBuf::from("../foo2/bar");

        let dest1_value = fs_util::read_link(fs.path.resolve(dest1))?;
        let dest2_value = fs_util::read_link(fs.path.resolve(dest2))?;
        let dest3_value = fs_util::read_link(fs.path.resolve(dest3))?;
        let dest4_value = fs_util::read_link(fs.path.resolve(dest4))?;
        let dest5_value = fs_util::read_link(fs.path.resolve(dest5))?;

        let contents1 = fs_util::read_to_string(fs.path.resolve(dest1))?;
        let contents2 = fs_util::read_to_string(fs.path.resolve(dest2))?;
        let contents3 = fs_util::read_to_string(fs.path.resolve(dest3))?;
        let contents4 = fs_util::read_to_string(
            fs.path
                .resolve(dest4)
                .join(ForwardRelativePath::new("file")?),
        )?;
        let contents5 = fs_util::read_to_string(
            fs.path
                .resolve(dest5)
                .join(ForwardRelativePath::new("file")?),
        )?;

        if cfg!(unix) {
            assert_eq!(dest1_expected, dest1_value);
            assert_eq!(dest2_expected, dest2_value);
            assert_eq!(dest3_expected, dest3_value);
            assert_eq!(dest4_expected, dest4_value);
            assert_eq!(dest5_expected, dest5_value);
        } else {
            // In Windows we use absolute path
            assert_eq!(fs.path.resolve(target1).as_path(), dest1_value);
            assert_eq!(fs.path.resolve(target1).as_path(), dest2_value);
            assert_eq!(fs.path.resolve(target1).as_path(), dest3_value);
            assert_eq!(fs.path.resolve(target2).as_path(), dest4_value);
            assert_eq!(fs.path.resolve(target2).as_path(), dest5_value);
        }

        assert_eq!("new foo1 contents", contents1);
        assert_eq!("new foo1 contents", contents2);
        assert_eq!("new foo1 contents", contents3);
        assert_eq!("new foo2 contents", contents4);
        assert_eq!("new foo2 contents", contents5);

        Ok(())
    }

    #[test]
    fn test_symlink_to_directory() -> anyhow::Result<()> {
        let fs = ProjectRootTemp::new()?;
        let source_dir = ProjectRelativePath::new("foo")?;
        let source_file = ProjectRelativePath::new("foo/file")?;
        let dest_dir = ProjectRelativePath::new("bar")?;
        let dest_file = ProjectRelativePath::new("bar/file")?;
        let new_file1 = ProjectRelativePath::new("bar/new_file")?;
        let new_file2 = ProjectRelativePath::new("foo/new_file")?;

        fs.path.write_file(source_file, "file content", false)?;
        fs.path.soft_link_relativized(source_dir, dest_dir)?;
        fs.path.write_file(new_file1, "new file content", false)?;

        let content = fs_util::read_to_string(fs.path.resolve(dest_file))?;
        let new_content = fs_util::read_to_string(fs.path.resolve(new_file2))?;

        assert_eq!("file content", content);
        assert_eq!("new file content", new_content);
        Ok(())
    }

    #[cfg(unix)]
    #[test]
    fn test_remove_readonly_path_recursive() -> anyhow::Result<()> {
        let fs = ProjectRootTemp::new()?;

        // We can delete a read-only file
        let file = ProjectRelativePath::new("foo/bar/link")?;
        fs.path.write_file(file, "Hello", false)?;
        let real_file = fs.path.resolve(file);
        let mut perm = fs_util::metadata(&real_file)?.permissions();
        perm.set_readonly(true);
        fs_util::set_permissions(&real_file, perm)?;
        fs.path.remove_path_recursive(file)?;
        assert!(!fs.path.resolve(file).exists());
        Ok(())
    }

    #[test]
    fn test_relativizes_paths_correct() -> anyhow::Result<()> {
        let fs = ProjectRootTemp::new()?;

        let test_cases = vec![
            ("foo/bar/baz", "notfoo/bar/quz", "../../foo/bar/baz"),
            (
                "foo/bar/baz",
                "notfoo/some/deep/tree/out",
                "../../../../foo/bar/baz",
            ),
            (
                "notfoo/bar/quz",
                "notfoo/some/deep/tree/out",
                "../../../bar/quz",
            ),
            ("foo/bar", "foo/baz", "bar"),
            ("bar", "foo/baz", "../bar"),
            ("foo/bar", "baz", "foo/bar"),
        ];

        for (target_str, dest_str, expected_str) in test_cases {
            let expected = PathBuf::from(expected_str);
            let target = ProjectRelativePath::new(target_str)?;
            let dest = ProjectRelativePath::new(dest_str)?;

            let actual =
                ProjectRoot::find_relative_path(&fs.path.resolve(target), &fs.path.resolve(dest));
            assert_eq!(
                expected,
                actual,
                "Expected path from {} to {} to be {}, got {}",
                target_str,
                dest_str,
                expected_str,
                actual.as_path().to_string_lossy()
            );
        }

        Ok(())
    }

    #[cfg(unix)]
    #[test]
    fn test_set_executable() -> anyhow::Result<()> {
        use std::os::unix::fs::PermissionsExt;

        let fs = ProjectRootTemp::new()?;

        // We can delete a read-only file
        let file = ProjectRelativePath::new("foo/bar/file")?;
        let real_file = fs.path.resolve(file);

        fs.path.write_file(file, "Hello", false)?;
        let perm = fs_util::metadata(&real_file)?.permissions();
        assert_eq!(perm.mode() & 0o111, 0);

        fs.path.set_executable(file)?;
        let perm = fs_util::metadata(&real_file)?.permissions();
        assert_eq!(perm.mode() & 0o111, 0o111);

        Ok(())
    }

    #[test]
    fn test_strip_project_root_simple() {
        let project_root = ProjectRootTemp::new().unwrap();
        assert_eq!(
            Path::new(""),
            project_root
                .path()
                .strip_project_root(project_root.path.root().as_abs_path())
                .unwrap()
        );
        assert_eq!(
            Path::new("foo"),
            project_root
                .path()
                .strip_project_root(&project_root.path.root().as_abs_path().join("foo"))
                .unwrap()
        );
        assert_eq!(
            Path::new("foo/bar"),
            project_root
                .path()
                .strip_project_root(&project_root.path.root().as_abs_path().join("foo/bar"))
                .unwrap()
        );
    }

    #[test]
    fn test_strip_project_root_complex() {
        if cfg!(windows) {
            return;
        }

        let project_root = ProjectRootTemp::new().unwrap();
        let temp_dir = tempfile::tempdir().unwrap();
        let temp_dir = AbsPath::new(temp_dir.path()).unwrap();

        fs_util::symlink(
            project_root.path.root(),
            AbsPath::new(&temp_dir.join("foo")).unwrap(),
        )
        .unwrap();
        assert_eq!(
            Path::new(""),
            project_root
                .path()
                .strip_project_root(&temp_dir.join("foo"))
                .unwrap()
        );
        assert_eq!(
            Path::new("bar"),
            project_root
                .path()
                .strip_project_root(&temp_dir.join("foo/bar"))
                .unwrap()
        );
    }

    #[test]
    fn test_relativize_any_bug() {
        if cfg!(windows) {
            return;
        }

        let project_root = ProjectRootTemp::new().unwrap();
        let project_root = project_root.path();

        fs_util::create_dir(project_root.root().as_abs_path().join("foo")).unwrap();
        fs_util::create_dir(project_root.root().as_abs_path().join("foo/bar")).unwrap();
        fs_util::create_dir(project_root.root().as_abs_path().join("link-target")).unwrap();
        fs_util::write(
            project_root.root().as_abs_path().join("link-target/fff"),
            "hello",
        )
        .unwrap();
        fs_util::symlink(
            Path::new("../../link-target"),
            project_root
                .root()
                .join(ForwardRelativePath::new("foo/bar/baz").unwrap()),
        )
        .unwrap();

        // Now explaining why the assertion in the end of the test is incorrect:
        // Existing path is resolved to non-existing path.

        let existing_path = "foo/bar/baz/../link-target/fff";
        let non_exist_path = "foo/bar/link-target/fff";
        assert!(
            fs_util::try_exists(project_root.root().as_abs_path().join(existing_path)).unwrap()
        );
        assert!(
            !fs_util::try_exists(project_root.root().as_abs_path().join(non_exist_path)).unwrap()
        );

        assert_eq!(
            ProjectRelativePath::new(non_exist_path).unwrap(),
            project_root
                .relativize_any(
                    project_root
                        .root()
                        .as_abs_path()
                        .join(Path::new(existing_path))
                )
                .unwrap()
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

//!
//! 'ProjectRelativePath's are normalized, platform agnostic, forward pointing
//! relative paths based at the `project root`.
//! The `project root` is an 'AbsPath' that corresponds to the root of the buck
//! process. This is not the current directory where the buck process is
//! invoked. It is the path of the root of the buck project, which defines the
//! buck version and configurations.
//!
//! The 'ProjectFilesystem' is the filesystem containing the `project root`
//! information. This file system is used to interact with the
//! 'ProjectRelativePath', and resolve the paths into a [`std::path::Path`] to
//! perform IO.
//!
//! Sample uses
//! ```
//! use buck2_core::fs::project::ProjectRoot;
//! use buck2_core::fs::project_rel_path::{ProjectRelativePathBuf, ProjectRelativePath};
//! use buck2_core::fs::paths::abs_norm_path::{AbsNormPathBuf, AbsNormPath};
//! use buck2_core::fs::paths::forward_rel_path::ForwardRelativePath;
//! use relative_path::RelativePath;
//! use std::{borrow::Cow, convert::TryFrom};
//!
//! let root = if cfg!(not(windows)) {
//!     AbsNormPathBuf::from("/usr/local/fbsource/".into())?
//! } else {
//!     AbsNormPathBuf::from("C:\\open\\fbsource\\".into())?
//! };
//! let some_path = if cfg!(not(windows)) {
//!     AbsNormPath::new("/usr/local/fbsource/buck/BUCK")?
//! } else {
//!     AbsNormPath::new("c:/open/fbsource/buck/BUCK")?
//! };
//!
//! let fs = ProjectRoot::new_unchecked(root);
//! let project_rel = fs.relativize(some_path)?;
//!
//! assert_eq!(Cow::Borrowed(ProjectRelativePath::new("buck/BUCK")?), project_rel);
//! assert_eq!(some_path.to_buf(), fs.resolve(project_rel.as_ref()));
//!
//! let rel_path = RelativePath::new("../src");
//! let project_rel_2 = project_rel.join_normalized(rel_path)?;
//! assert_eq!(ProjectRelativePathBuf::try_from("buck/src".to_owned())?, project_rel_2);
//!
//! assert_eq!(some_path.join_normalized(rel_path)?, fs.resolve(&project_rel_2).to_buf());
//!
//! # anyhow::Ok(())
//! ```
//!

use std::borrow::Borrow;
use std::ops::Deref;
use std::path::Path;
use std::path::PathBuf;

use allocative::Allocative;
use derivative::Derivative;
use ref_cast::RefCast;
use relative_path::RelativePath;
use relative_path::RelativePathBuf;
use serde::Deserialize;
use serde::Serialize;

use crate::fs::paths::file_name::FileName;
use crate::fs::paths::fmt::quoted_display;
use crate::fs::paths::forward_rel_path::ForwardRelativePath;
use crate::fs::paths::forward_rel_path::ForwardRelativePathBuf;
use crate::fs::paths::forward_rel_path::ForwardRelativePathIter;

/// A un-owned forward pointing, fully normalized path that is relative to the
/// project root.
#[derive(
    derive_more::Display,
    Derivative,
    Hash,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    RefCast
)]
#[derivative(Debug)]
#[repr(transparent)]
pub struct ProjectRelativePath(
    // TODO(nga): make private.
    #[derivative(Debug(format_with = "quoted_display"))] pub(crate) ForwardRelativePath,
);

/// The owned version of the 'ProjectRelativePath'
#[derive(Clone, derive_more::Display, Derivative)]
// split in two because formatters don't agree
#[derive(
    Hash,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Serialize,
    Deserialize,
    Allocative
)]
#[derivative(Debug)]
pub struct ProjectRelativePathBuf(
    #[derivative(Debug(format_with = "quoted_display"))] ForwardRelativePathBuf,
);

impl AsRef<ForwardRelativePath> for ProjectRelativePath {
    fn as_ref(&self) -> &ForwardRelativePath {
        &self.0
    }
}

impl AsRef<RelativePath> for ProjectRelativePath {
    fn as_ref(&self) -> &RelativePath {
        self.0.as_ref()
    }
}

impl AsRef<ForwardRelativePath> for ProjectRelativePathBuf {
    fn as_ref(&self) -> &ForwardRelativePath {
        &self.0
    }
}

impl AsRef<RelativePath> for ProjectRelativePathBuf {
    fn as_ref(&self) -> &RelativePath {
        self.0.as_ref()
    }
}

impl AsRef<ForwardRelativePathBuf> for ProjectRelativePathBuf {
    fn as_ref(&self) -> &ForwardRelativePathBuf {
        &self.0
    }
}

impl ProjectRelativePath {
    pub fn unchecked_new<S: ?Sized + AsRef<str>>(s: &S) -> &Self {
        ProjectRelativePath::ref_cast(ForwardRelativePath::unchecked_new(s))
    }

    pub fn empty() -> &'static Self {
        ProjectRelativePath::unchecked_new("")
    }

    /// Creates an 'ProjectRelativePath' if the given string represents a
    /// forward, normalized relative path, otherwise error.
    ///
    /// ```
    /// use std::path::Path;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// assert!(ProjectRelativePath::new("foo/bar").is_ok());
    /// assert!(ProjectRelativePath::new("").is_ok());
    /// assert!(ProjectRelativePath::new("/abs/bar").is_err());
    /// assert!(ProjectRelativePath::new("normalize/./bar").is_err());
    /// assert!(ProjectRelativePath::new("normalize/../bar").is_err());
    ///
    /// assert!(ProjectRelativePath::new(Path::new("foo/bar")).is_ok());
    /// assert!(ProjectRelativePath::new(Path::new("")).is_ok());
    /// assert!(ProjectRelativePath::new(Path::new("/abs/bar")).is_err());
    /// assert!(ProjectRelativePath::new(Path::new("normalize/./bar")).is_err());
    /// assert!(ProjectRelativePath::new(Path::new("normalize/../bar")).is_err());
    /// ```
    pub fn new<P: ?Sized + AsRef<Path>>(p: &P) -> anyhow::Result<&ProjectRelativePath> {
        Ok(ProjectRelativePath::ref_cast(ForwardRelativePath::new(p)?))
    }

    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }

    pub fn as_forward_relative_path(&self) -> &ForwardRelativePath {
        &self.0
    }

    #[inline]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Creates an owned 'ProjectRelativePathBuf' with path adjoined to self.
    ///
    /// ```
    /// use std::path::Path;
    /// use buck2_core::fs::paths::forward_rel_path::ForwardRelativePath;
    /// use buck2_core::fs::project_rel_path::{ProjectRelativePathBuf, ProjectRelativePath};
    ///
    /// let path = ProjectRelativePath::new("foo/bar")?;
    /// let other = ForwardRelativePath::new("baz")?;
    /// assert_eq!(ProjectRelativePathBuf::unchecked_new("foo/bar/baz".to_owned()), path.join(other));
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn join<P: AsRef<ForwardRelativePath>>(&self, path: P) -> ProjectRelativePathBuf {
        ProjectRelativePathBuf(self.0.join(path.as_ref()))
    }

    /// Returns a relative path of the parent directory
    ///
    /// ```
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// assert_eq!(
    ///     Some(ProjectRelativePath::new("foo")?),
    ///     ProjectRelativePath::new("foo/bar")?.parent()
    /// );
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn parent(&self) -> Option<&ProjectRelativePath> {
        self.0.parent().map(ProjectRelativePath::ref_cast)
    }

    /// Returns the final component of the `ProjectRelativePath`, if there is
    /// one.
    ///
    /// If the path is a normal file, this is the file name. If it's the path of
    /// a directory, this is the directory name.
    ///
    /// ```
    /// use buck2_core::fs::paths::file_name::FileName;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// assert_eq!(Some(FileName::unchecked_new("bin")), ProjectRelativePath::new("usr/bin")?.file_name());
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn file_name(&self) -> Option<&FileName> {
        self.0.file_name()
    }

    /// Returns a 'ForwardRelativePath' that, when joined onto `base`, yields
    /// `self`.
    ///
    /// Error if `base` is not a prefix of `self` or the returned
    /// path is not a 'ForwardRelativePath'
    ///
    /// ```
    /// use buck2_core::fs::paths::forward_rel_path::ForwardRelativePath;
    ///
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// let path = ProjectRelativePath::new("test/haha/foo.txt")?;
    ///
    /// assert_eq!(
    ///     path.strip_prefix(ProjectRelativePath::new("test")?)?,
    ///     ForwardRelativePath::new("haha/foo.txt")?
    /// );
    /// assert_eq!(path.strip_prefix(ProjectRelativePath::new("asdf")?).is_err(), true);
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn strip_prefix<'a, P: ?Sized>(
        &'a self,
        base: &'a P,
    ) -> anyhow::Result<&'a ForwardRelativePath>
    where
        P: AsRef<ProjectRelativePath>,
    {
        self.0.strip_prefix(&base.as_ref().0)
    }

    pub fn strip_prefix_opt<'a, P: ?Sized>(&'a self, base: &'a P) -> Option<&'a ForwardRelativePath>
    where
        P: AsRef<ProjectRelativePath>,
    {
        self.0.strip_prefix_opt(&base.as_ref().0)
    }

    /// Determines whether `base` is a prefix of `self`.
    ///
    /// ```
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// let path = ProjectRelativePath::new("some/foo")?;
    ///
    /// assert!(path.starts_with(ProjectRelativePath::new("some")?));
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn starts_with<P: AsRef<ProjectRelativePath>>(&self, base: P) -> bool {
        self.0.starts_with(&base.as_ref().0)
    }

    /// Determines whether `child` is a suffix of `self`.
    /// Only considers whole path components to match.
    ///
    /// ```
    /// use std::path::Path;
    /// use buck2_core::fs::paths::forward_rel_path::ForwardRelativePath;
    ///
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// let path = ProjectRelativePath::new("some/foo")?;
    ///
    /// assert!(path.ends_with(ForwardRelativePath::new("foo").unwrap()));
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn ends_with<P: AsRef<ForwardRelativePath>>(&self, child: P) -> bool {
        self.0.ends_with(child.as_ref())
    }

    /// Extracts the stem (non-extension) portion of [`self.file_name`].
    ///
    /// The stem is:
    ///
    /// * [`None`], if there is no file name;
    /// * The entire file name if there is no embedded `.`;
    /// * The entire file name if the file name begins with `.` and has no other
    ///   `.`s within;
    /// * Otherwise, the portion of the file name before the final `.`
    ///
    /// ```
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// let path = ProjectRelativePath::new("foo.rs")?;
    ///
    /// assert_eq!(Some("foo"), path.file_stem());
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn file_stem(&self) -> Option<&str> {
        self.0.file_stem()
    }

    /// Extracts the extension of [`self.file_name`], if possible.
    ///
    /// ```
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// assert_eq!(Some("rs"), ProjectRelativePath::new("hi/foo.rs")?.extension());
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn extension(&self) -> Option<&str> {
        self.0.extension()
    }

    /// Build an owned `ProjectRelativePathBuf`, joined with the given path and
    /// normalized.
    ///
    /// ```
    /// use std::convert::TryFrom;
    /// use buck2_core::fs::project_rel_path::{ProjectRelativePath, ProjectRelativePathBuf};
    ///
    /// assert_eq!(
    ///     ProjectRelativePath::new("foo/bar")?.join_normalized("../baz.txt")?,
    ///     ProjectRelativePathBuf::unchecked_new("foo/baz.txt".into()),
    /// );
    ///
    /// assert_eq!(
    ///     ProjectRelativePath::new("foo")?.join_normalized("../../baz.txt").is_err(),
    ///     true
    /// );
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn join_normalized<P: AsRef<RelativePath>>(
        &self,
        path: P,
    ) -> anyhow::Result<ProjectRelativePathBuf> {
        let inner = self.0.join_normalized(path)?;
        // TODO need verify?
        Ok(ProjectRelativePathBuf(inner))
    }

    /// Iterator over the components of this path
    ///
    /// ```
    /// use buck2_core::fs::paths::file_name::FileName;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// let p = ProjectRelativePath::new("foo/bar/baz")?;
    /// let mut it = p.iter();
    ///
    /// assert_eq!(
    ///     it.next(),
    ///     Some(FileName::unchecked_new("foo"))
    /// );
    /// assert_eq!(
    ///     it.next(),
    ///     Some(FileName::unchecked_new("bar"))
    /// );
    /// assert_eq!(
    ///     it.next(),
    ///     Some(FileName::unchecked_new("baz"))
    /// );
    /// assert_eq!(
    ///     it.next(),
    ///     None
    /// );
    ///
    /// # anyhow::Ok(())
    /// ```
    pub fn iter(&self) -> ForwardRelativePathIter {
        self.0.iter()
    }

    pub fn to_buf(&self) -> ProjectRelativePathBuf {
        self.to_owned()
    }
}

impl<'a> From<&'a ForwardRelativePath> for &'a ProjectRelativePath {
    ///
    /// ```
    /// use buck2_core::fs::paths::forward_rel_path::ForwardRelativePath;
    /// use std::convert::From;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// let f = ForwardRelativePath::new("foo")?;
    ///
    /// assert_eq!(<&ProjectRelativePath>::from(f), ProjectRelativePath::new("foo")?);
    ///
    /// # anyhow::Ok(())
    /// ```
    fn from(p: &'a ForwardRelativePath) -> &'a ProjectRelativePath {
        ProjectRelativePath::ref_cast(p)
    }
}

impl ProjectRelativePathBuf {
    pub fn unchecked_new(s: String) -> Self {
        Self(ForwardRelativePathBuf::unchecked_new(s))
    }

    pub fn testing_new(path: &str) -> ProjectRelativePathBuf {
        ProjectRelativePathBuf::try_from(path.to_owned()).unwrap()
    }

    /// Creates a new 'ProjectRelativePathBuf' with a given capacity used to create the internal
    /// 'String'. See 'with_capacity' defined on 'ForwardRelativePathBuf'
    pub fn with_capacity(cap: usize) -> Self {
        Self(ForwardRelativePathBuf::with_capacity(cap))
    }

    /// Returns the capacity of the underlying 'ForwardRelativePathBuf'
    pub fn capacity(&self) -> usize {
        self.0.capacity()
    }

    /// Invokes 'reserve' on the underlying 'ForwardRelativePathBuf'
    pub fn reserve(&mut self, additional: usize) {
        self.0.reserve(additional)
    }

    /// Invokes 'shrink_to_fit' on the underlying 'ForwardRelativePathBuf'
    pub fn shrink_to_fit(&mut self) {
        self.0.shrink_to_fit()
    }

    /// Invokes 'shrink_to' on the underlying 'String'
    pub fn shrink_to(&mut self, min_capacity: usize) {
        self.0.shrink_to(min_capacity)
    }

    /// Pushes a `ForwardRelativePath` to the existing buffer
    pub fn push<P: AsRef<ForwardRelativePath>>(&mut self, path: P) {
        self.0.push(path)
    }

    /// Pushes a `RelativePath` to the existing buffer, normalizing it
    pub fn push_normalized<P: AsRef<RelativePath>>(&mut self, path: P) -> anyhow::Result<()> {
        self.0.push_normalized(path)
    }

    pub fn into_forward_relative_path_buf(self) -> ForwardRelativePathBuf {
        self.0
    }
}

impl From<ForwardRelativePathBuf> for ProjectRelativePathBuf {
    fn from(p: ForwardRelativePathBuf) -> Self {
        Self(p)
    }
}

impl From<ProjectRelativePathBuf> for ForwardRelativePathBuf {
    fn from(p: ProjectRelativePathBuf) -> Self {
        p.0
    }
}

impl From<ProjectRelativePathBuf> for RelativePathBuf {
    fn from(p: ProjectRelativePathBuf) -> Self {
        p.0.into()
    }
}

impl<'a> TryFrom<&'a str> for &'a ProjectRelativePath {
    type Error = anyhow::Error;

    /// no allocation conversion
    ///
    /// ```
    /// use std::convert::TryFrom;
    /// use buck2_core::fs::paths::forward_rel_path::ForwardRelativePath;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// assert!(<&ProjectRelativePath>::try_from("foo/bar").is_ok());
    /// assert!(<&ProjectRelativePath>::try_from("").is_ok());
    /// assert!(<&ProjectRelativePath>::try_from("/abs/bar").is_err());
    /// assert!(<&ProjectRelativePath>::try_from("normalize/./bar").is_err());
    /// assert!(<&ProjectRelativePath>::try_from("normalize/../bar").is_err());
    /// ```
    fn try_from(s: &'a str) -> anyhow::Result<&'a ProjectRelativePath> {
        Ok(ProjectRelativePath::ref_cast(ForwardRelativePath::new(s)?))
    }
}

impl<'a> TryFrom<&'a RelativePath> for &'a ProjectRelativePath {
    type Error = anyhow::Error;

    /// no allocation conversion
    ///
    /// ```
    /// use std::convert::TryFrom;
    /// use buck2_core::fs::paths::RelativePath;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePath;
    ///
    /// assert!(<&ProjectRelativePath>::try_from(RelativePath::new("foo/bar")).is_ok());
    /// assert!(<&ProjectRelativePath>::try_from(RelativePath::new("")).is_ok());
    /// assert!(<&ProjectRelativePath>::try_from(RelativePath::new("normalize/./bar")).is_err());
    /// assert!(<&ProjectRelativePath>::try_from(RelativePath::new("normalize/../bar")).is_err());
    /// ```
    fn try_from(s: &'a RelativePath) -> anyhow::Result<&'a ProjectRelativePath> {
        Ok(ProjectRelativePath::ref_cast(ForwardRelativePath::new(
            s.as_str(),
        )?))
    }
}

impl TryFrom<String> for ProjectRelativePathBuf {
    type Error = anyhow::Error;

    /// no allocation conversion
    ///
    /// ```
    /// use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
    /// use std::convert::TryFrom;
    ///
    /// assert!(ProjectRelativePathBuf::try_from("foo/bar".to_owned()).is_ok());
    /// assert!(ProjectRelativePathBuf::try_from("".to_owned()).is_ok());
    /// assert!(ProjectRelativePathBuf::try_from("/abs/bar".to_owned()).is_err());
    /// assert!(ProjectRelativePathBuf::try_from("normalize/./bar".to_owned()).is_err());
    /// assert!(ProjectRelativePathBuf::try_from("normalize/../bar".to_owned()).is_err());
    /// ```
    fn try_from(s: String) -> anyhow::Result<ProjectRelativePathBuf> {
        Ok(ProjectRelativePathBuf::from(
            ForwardRelativePathBuf::try_from(s)?,
        ))
    }
}

impl TryFrom<RelativePathBuf> for ProjectRelativePathBuf {
    type Error = anyhow::Error;

    /// no allocation conversion (TODO make ForwardRelativePath a no allocation
    /// conversion)
    ///
    /// ```
    /// use buck2_core::fs::paths::RelativePathBuf;
    /// use std::convert::TryFrom;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
    ///
    /// assert!(ProjectRelativePathBuf::try_from(RelativePathBuf::from("foo/bar")).is_ok());
    /// assert!(ProjectRelativePathBuf::try_from(RelativePathBuf::from("")).is_ok());
    /// assert!(ProjectRelativePathBuf::try_from(RelativePathBuf::from("normalize/./bar")).is_err());
    /// assert!(ProjectRelativePathBuf::try_from(RelativePathBuf::from("normalize/../bar")).is_err());
    /// ```
    fn try_from(p: RelativePathBuf) -> anyhow::Result<ProjectRelativePathBuf> {
        Ok(ProjectRelativePathBuf::from(
            ForwardRelativePathBuf::try_from(p)?,
        ))
    }
}

impl TryFrom<PathBuf> for ProjectRelativePathBuf {
    type Error = anyhow::Error;

    /// no allocation conversion
    ///
    /// ```
    ///
    /// use std::convert::TryFrom;
    /// use std::path::PathBuf;
    /// use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
    ///
    /// assert!(ProjectRelativePathBuf::try_from(PathBuf::from("foo/bar")).is_ok());
    /// assert!(ProjectRelativePathBuf::try_from(PathBuf::from("")).is_ok());
    /// assert!(ProjectRelativePathBuf::try_from(PathBuf::from("/abs/bar")).is_err());
    /// assert!(ProjectRelativePathBuf::try_from(PathBuf::from("normalize/./bar")).is_err());
    /// assert!(ProjectRelativePathBuf::try_from(PathBuf::from("normalize/../bar")).is_err());
    /// ```
    fn try_from(p: PathBuf) -> anyhow::Result<ProjectRelativePathBuf> {
        Ok(ProjectRelativePathBuf(ForwardRelativePathBuf::try_from(p)?))
    }
}

impl ToOwned for ProjectRelativePath {
    type Owned = ProjectRelativePathBuf;

    fn to_owned(&self) -> ProjectRelativePathBuf {
        ProjectRelativePathBuf(self.0.to_owned())
    }
}

impl AsRef<ProjectRelativePath> for ProjectRelativePath {
    fn as_ref(&self) -> &ProjectRelativePath {
        self
    }
}

impl AsRef<ProjectRelativePath> for ProjectRelativePathBuf {
    fn as_ref(&self) -> &ProjectRelativePath {
        ProjectRelativePath::ref_cast(&self.0)
    }
}

impl Borrow<ProjectRelativePath> for ProjectRelativePathBuf {
    fn borrow(&self) -> &ProjectRelativePath {
        self.as_ref()
    }
}

impl Deref for ProjectRelativePathBuf {
    type Target = ProjectRelativePath;

    fn deref(&self) -> &ProjectRelativePath {
        ProjectRelativePath::ref_cast(&self.0)
    }
}

#[cfg(test)]
mod tests {
    use crate::fs::project_rel_path::ProjectRelativePath;
    use crate::fs::project_rel_path::ProjectRelativePathBuf;

    #[test]
    fn path_display_is_readable() -> anyhow::Result<()> {
        let buf = ProjectRelativePathBuf::try_from("foo/bar".to_owned())?;
        assert_eq!("foo/bar", format!("{}", buf));
        assert_eq!("ProjectRelativePathBuf(\"foo/bar\")", format!("{:?}", buf));
        let refpath: &ProjectRelativePath = &buf;
        assert_eq!("foo/bar", format!("{}", refpath));
        assert_eq!("ProjectRelativePath(\"foo/bar\")", format!("{:?}", refpath));

        Ok(())
    }

    #[test]
    fn path_is_comparable() -> anyhow::Result<()> {
        let path1_buf = ProjectRelativePathBuf::try_from("foo".to_owned())?;
        let path2_buf = ProjectRelativePathBuf::try_from("foo".to_owned())?;
        let path3_buf = ProjectRelativePathBuf::try_from("bar".to_owned())?;

        let path1 = ProjectRelativePath::new("foo")?;
        let path2 = ProjectRelativePath::new("foo")?;
        let path3 = ProjectRelativePath::new("bar")?;

        let str2 = "foo";
        let str3 = "bar";
        let str_abs = "/ble";

        let string2 = "foo".to_owned();
        let string3 = "bar".to_owned();
        let string_abs = "/ble".to_owned();

        assert_eq!(path1_buf, path2_buf);
        assert_ne!(path1_buf, path3_buf);

        assert_eq!(path1, path2);
        assert_ne!(path1, path3);

        assert_eq!(path1_buf, path2);
        assert_ne!(path1, path3_buf);

        assert_eq!(path1_buf, str2);
        assert_ne!(path1_buf, str3);
        assert_ne!(path1_buf, str_abs);

        assert_eq!(path1, str2);
        assert_ne!(path1, str3);
        assert_ne!(path1, str_abs);

        assert_eq!(path1_buf, string2);
        assert_ne!(path1_buf, string3);
        assert_ne!(path1_buf, string_abs);

        assert_eq!(path1, string2);
        assert_ne!(path1, string3);
        assert_ne!(path1, string_abs);

        Ok(())
    }

    #[test]
    fn test_serde() {
        fn test_roundtrip(path: &str, json: &str) {
            let path = ProjectRelativePathBuf::try_from(path.to_owned()).unwrap();
            assert_eq!(json, serde_json::to_string(&path).unwrap());
            assert_eq!(
                path,
                serde_json::from_str::<ProjectRelativePathBuf>(json).unwrap()
            );
        }

        test_roundtrip("", r#""""#);
        test_roundtrip("foo", r#""foo""#);
        test_roundtrip("foo/bar", r#""foo/bar""#);

        let err = serde_json::from_str::<ProjectRelativePathBuf>(r#""a//b""#)
            .unwrap_err()
            .to_string();
        assert!(err.contains("expected a normalized path"), "{}", err);
    }
}