rust/hg-core/src/checkexec.rs
author Arseniy Alekseyev <aalekseyev@janestreet.com>
Thu, 05 Jan 2023 17:15:03 +0000
changeset 49894 678588b01af1
child 49895 07792fd1837f
permissions -rw-r--r--
rhg: implement checkexec to support weird filesystems In particular, some of our repos are stored on a fileserver that simulates POSIX permissions poorly, in such a way that prevents the removal of execute permission. This causes rhg show a spurious unclean status, even though python hg reports the repo as clean. We fix this by making rhg implement the ~same checkexec logic that python hg does.

use std::fs;
use std::io;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::Path;

// This is a rust rewrite of [checkexec] function from [posix.py]

const EXECFLAGS: u32 = 0o111;

fn is_executable(path: impl AsRef<Path>) -> Result<bool, io::Error> {
    let metadata = fs::metadata(path)?;
    let mode = metadata.mode();
    Ok(mode & EXECFLAGS != 0)
}

fn make_executable(path: impl AsRef<Path>) -> Result<(), io::Error> {
    let mode = fs::metadata(path.as_ref())?.mode();
    fs::set_permissions(
        path,
        fs::Permissions::from_mode((mode & 0o777) | EXECFLAGS),
    )?;
    Ok(())
}

fn copy_mode(
    src: impl AsRef<Path>,
    dst: impl AsRef<Path>,
) -> Result<(), io::Error> {
    let mode = match fs::symlink_metadata(src) {
        Ok(metadata) => metadata.mode(),
        Err(e) if e.kind() == io::ErrorKind::NotFound =>
        // copymode in python has a more complicated handling of FileNotFound
        // error, which we don't need because all it does is applying
        // umask, which the OS already does when we mkdir.
        {
            return Ok(())
        }
        Err(e) => return Err(e),
    };
    fs::set_permissions(dst, fs::Permissions::from_mode(mode))?;
    Ok(())
}

fn check_exec_impl(path: impl AsRef<Path>) -> Result<bool, io::Error> {
    let basedir = path.as_ref().join(".hg");
    let cachedir = basedir.join("wcache");
    let storedir = basedir.join("store");

    if !cachedir.exists() {
        fs::create_dir(&cachedir)
            .and_then(|()| {
                if storedir.exists() {
                    copy_mode(&storedir, &cachedir)
                } else {
                    copy_mode(&basedir, &cachedir)
                }
            })
            .ok();
    }

    let leave_file: bool;
    let checkdir: &Path;
    let checkisexec = cachedir.join("checkisexec");
    let checknoexec = cachedir.join("checknoexec");
    if cachedir.is_dir() {
        match is_executable(&checkisexec) {
            Err(e) if e.kind() == io::ErrorKind::NotFound => (),
            Err(e) => return Err(e),
            Ok(is_exec) => {
                if is_exec {
                    let noexec_is_exec = match is_executable(&checknoexec) {
                        Err(e) if e.kind() == io::ErrorKind::NotFound => {
                            fs::write(&checknoexec, "")?;
                            is_executable(&checknoexec)?
                        }
                        Err(e) => return Err(e),
                        Ok(exec) => exec,
                    };
                    if !noexec_is_exec {
                        // check-exec is exec and check-no-exec is not exec
                        return Ok(true);
                    }
                    fs::remove_file(&checknoexec)?;
                }
                fs::remove_file(&checkisexec)?;
            }
        }
        checkdir = &cachedir;
        leave_file = true;
    } else {
        checkdir = path.as_ref();
        leave_file = false;
    };

    let tmp_file = tempfile::NamedTempFile::new_in(checkdir)?;
    if !is_executable(tmp_file.path())? {
        make_executable(tmp_file.path())?;
        if is_executable(tmp_file.path())? {
            if leave_file {
                tmp_file.persist(checkisexec).ok();
            }
            return Ok(true);
        }
    }

    Ok(false)
}

pub fn check_exec(path: impl AsRef<Path>) -> bool {
    check_exec_impl(path).unwrap_or(false)
}