467 lines · 16.8 KB · Rust
RawDownload
use std::{    borrow::Cow,    io::Write,    path::{Path, PathBuf},}; use bstr::BStr;use gix_filter::{driver::apply::MaybeDelayed, pipeline::convert::ToWorktreeOutcome};use gix_index::{Entry, entry::Stat};use gix_object::FindExt;use gix_worktree::Stack;use io_close::Close; pub struct Context<'a, Find> {    pub objects: &'a mut Find,    pub path_cache: &'a mut Stack,    pub filters: &'a mut gix_filter::Pipeline,    pub buf: &'a mut Vec<u8>,} /// A delayed result of a long-running filter process, which is made available as stream.pub struct DelayedFilteredStream<'a> {    /// The key identifying the driver program    pub key: gix_filter::driver::Key,    /// Whether the filesystem supports executable bits.    pub fs_supports_executable_bit: bool,    /// The validated path on disk at which the file should be placed.    pub validated_file_path: PathBuf,    /// The entry to adjust with the file we will write.    pub entry: &'a mut gix_index::Entry,    /// The relative path at which the entry resides (for use when querying the delayed entry).    pub entry_path: &'a BStr,    /// An unavailable-encoding error to report once the delayed file was written.    pub ignored_filter_error: Option<gix_filter::pipeline::convert::configuration::Error>,} pub enum Outcome<'a> {    /// The file was written.    Written {        /// The amount of written bytes.        bytes: usize,        /// An unavailable-encoding error ignored during conversion.        ignored_filter_error: Option<gix_filter::pipeline::convert::configuration::Error>,    },    /// The will be ready later.    Delayed(DelayedFilteredStream<'a>),} impl Outcome<'_> {    /// Return ourselves as (in-memory) bytes if possible.    pub fn as_bytes(&self) -> Option<usize> {        match self {            Outcome::Written { bytes, .. } => Some(*bytes),            Outcome::Delayed(_) => None,        }    }     /// Take the filter error that was ignored while producing this outcome.    pub fn take_ignored_filter_error(&mut self) -> Option<gix_filter::pipeline::convert::configuration::Error> {        match self {            Outcome::Written {                ignored_filter_error, ..            } => ignored_filter_error.take(),            Outcome::Delayed(_) => None,        }    }} #[cfg_attr(not(unix), allow(unused_variables))]pub fn checkout<'entry, Find>(    entry: &'entry mut Entry,    entry_path: &'entry BStr,    Context {        objects,        filters,        path_cache,        buf,    }: Context<'_, Find>,    crate::checkout::chunk::Options {        fs: gix_fs::Capabilities {            symlink,            executable_bit,            ..        },        destination_is_initially_empty,        overwrite_existing,        filter_process_delay,        ..    }: crate::checkout::chunk::Options,) -> Result<Outcome<'entry>, crate::checkout::Error>where    Find: gix_object::Find,{    let mut ignored_filter_error = None;    let dest_relative = gix_path::try_from_bstr(entry_path).map_err(|_| crate::checkout::Error::IllformedUtf8 {        path: entry_path.to_owned(),    })?;    let path_cache = path_cache.at_path(dest_relative.as_ref(), Some(entry.mode), &*objects)?;    let dest = path_cache.path();     let object_size = match entry.mode {        gix_index::entry::Mode::FILE | gix_index::entry::Mode::FILE_EXECUTABLE => {            let obj = (*objects)                .find_blob(&entry.id, buf)                .map_err(|err| crate::checkout::Error::Find {                    err,                    path: dest.to_path_buf(),                })?;             let converted = filters.convert_to_worktree_with_options(                obj.data,                entry_path,                &mut |_, attrs| {                    path_cache.matching_attributes(attrs);                },                gix_filter::pipeline::convert::to_worktree::Options {                    delay: filter_process_delay,                    ..Default::default()                },            )?;            ignored_filter_error = converted.ignored_error;            let filtered = converted.output;            let (num_bytes, file, executable_bit_change) = match filtered {                ToWorktreeOutcome::Unchanged(buf) | ToWorktreeOutcome::Buffer(buf) => {                    let (mut file, flag) = open_file(                        dest,                        destination_is_initially_empty,                        overwrite_existing,                        executable_bit,                        entry.mode,                    )?;                    file.write_all(buf)?;                    (buf.len(), file, flag)                }                ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut filtered)) => {                    let (mut file, flag) = open_file(                        dest,                        destination_is_initially_empty,                        overwrite_existing,                        executable_bit,                        entry.mode,                    )?;                    let num_bytes = std::io::copy(&mut filtered, &mut file)? as usize;                    (num_bytes, file, flag)                }                ToWorktreeOutcome::Process(MaybeDelayed::Delayed(key)) => {                    return Ok(Outcome::Delayed(DelayedFilteredStream {                        key,                        fs_supports_executable_bit: executable_bit,                        validated_file_path: dest.to_owned(),                        entry,                        entry_path,                        ignored_filter_error,                    }));                }            };             // For possibly existing, overwritten files, we must change the file mode explicitly.            finalize_entry(entry, file, num_bytes as u64, executable_bit_change)?;            num_bytes        }        gix_index::entry::Mode::SYMLINK => {            let obj = (*objects)                .find_blob(&entry.id, buf)                .map_err(|err| crate::checkout::Error::Find {                    err,                    path: dest.to_path_buf(),                })?;            if symlink {                #[cfg_attr(not(windows), allow(unused_mut))]                let mut symlink_destination = Cow::Borrowed(                    gix_path::try_from_byte_slice(obj.data)                        .map_err(|_| crate::checkout::Error::IllformedUtf8 { path: obj.data.into() })?,                );                #[cfg(windows)]                {                    symlink_destination = gix_path::to_native_path_on_windows(gix_path::into_bstr(symlink_destination))                }                 try_op_or_unlink(dest, overwrite_existing, |p| {                    gix_fs::symlink::create(symlink_destination.as_ref(), p)                })?;            } else {                let mut file = try_op_or_unlink(dest, overwrite_existing, |p| {                    open_options(p, destination_is_initially_empty, overwrite_existing).open(dest)                })?;                file.write_all(obj.data)?;                file.close()?;            }             entry.stat = Stat::from_fs(&gix_index::fs::Metadata::from_path_no_follow(dest)?)?;            obj.data.len()        }        gix_index::entry::Mode::DIR => {            gix_features::trace::warn!(                "Skipped sparse directory at '{entry_path}' ({id}) as it cannot yet be handled",                id = entry.id            );            0        }        gix_index::entry::Mode::COMMIT => {            gix_features::trace::warn!(                "Skipped submodule at '{entry_path}' ({id}) as it cannot yet be handled",                id = entry.id            );            0        }        _ => unreachable!(),    };    Ok(Outcome::Written {        bytes: object_size,        ignored_filter_error,    })} /// Note that this works only because we assume to not race ourselves when symlinks are involved, and we do this by/// delaying symlink creation to the end and will always do that sequentially./// It's still possible to fall for a race if other actors create symlinks in our path, but that's nothing to defend against.fn try_op_or_unlink<T>(    path: &Path,    overwrite_existing: bool,    op: impl Fn(&Path) -> std::io::Result<T>,) -> std::io::Result<T> {    if overwrite_existing {        match op(path) {            Ok(res) => Ok(res),            Err(err) if gix_fs::symlink::is_collision_error(&err) => {                try_unlink_path_recursively(path, &std::fs::symlink_metadata(path)?)?;                op(path)            }            Err(err) => Err(err),        }    } else {        op(path)    }} fn try_unlink_path_recursively(path: &Path, path_meta: &std::fs::Metadata) -> std::io::Result<()> {    if path_meta.is_dir() {        std::fs::remove_dir_all(path)    } else if path_meta.file_type().is_symlink() {        gix_fs::symlink::remove(path)    } else {        std::fs::remove_file(path)    }} #[cfg(not(debug_assertions))]fn debug_assert_dest_is_no_symlink(_path: &Path) {} /// This is a debug assertion as we expect the machinery calling this to prevent this possibility in the first place#[cfg(debug_assertions)]fn debug_assert_dest_is_no_symlink(path: &Path) {    if let Ok(meta) = path.metadata() {        debug_assert!(            !meta.file_type().is_symlink(),            "BUG: should not ever allow to overwrite/write-into the target of a symbolic link: {}",            path.display()        );    }} fn open_options(path: &Path, destination_is_initially_empty: bool, overwrite_existing: bool) -> std::fs::OpenOptions {    if overwrite_existing || !destination_is_initially_empty {        debug_assert_dest_is_no_symlink(path);    }    let mut options = gix_features::fs::open_options_no_follow();    options        .create_new(destination_is_initially_empty && !overwrite_existing)        .create(!destination_is_initially_empty || overwrite_existing)        .write(true)        .truncate(true);    options} pub(crate) fn open_file(    path: &Path,    destination_is_initially_empty: bool,    overwrite_existing: bool,    fs_supports_executable_bit: bool,    entry_mode: gix_index::entry::Mode,) -> std::io::Result<(std::fs::File, ExecutableBitChange)> {    #[cfg_attr(windows, allow(unused_mut))]    let mut options = open_options(path, destination_is_initially_empty, overwrite_existing);    let needs_executable_bit = fs_supports_executable_bit && entry_mode == gix_index::entry::Mode::FILE_EXECUTABLE;    #[cfg(unix)]    let executable_bit_change = if needs_executable_bit && destination_is_initially_empty {        use std::os::unix::fs::OpenOptionsExt;        // Note that these only work if the file was newly created, but won't if it's already        // existing, possibly without the executable bit set. Thus we do this only if the file is new.        options.mode(0o777);        ExecutableBitChange::NoChange    } else if !fs_supports_executable_bit || destination_is_initially_empty {        ExecutableBitChange::NoChange    } else if needs_executable_bit {        ExecutableBitChange::Set    } else {        ExecutableBitChange::Remove    };    //  not supported on windows    #[cfg(windows)]    let executable_bit_change = ExecutableBitChange::NoChange;    try_op_or_unlink(path, overwrite_existing, |p| options.open(p)).map(|f| (f, executable_bit_change))} /// Close `file` and store its stats in `entry`, possibly adjusting whether `file` is executable.////// `desired_bytes` is the amount of bytes Git thinks the file should have after writing.pub(crate) fn finalize_entry(    entry: &mut gix_index::Entry,    file: std::fs::File,    desired_bytes: u64,    #[cfg_attr(windows, allow(unused_variables))] executable_bit_change: ExecutableBitChange,) -> Result<(), crate::checkout::Error> {    // For possibly existing, overwritten files, we must change the file mode explicitly to match the index.    #[cfg(unix)]    match executable_bit_change {        ExecutableBitChange::NoChange => {}        ExecutableBitChange::Set => adjust_executable_bits(&file, true)?,        ExecutableBitChange::Remove => adjust_executable_bits(&file, false)?,    }     let md = &gix_index::fs::Metadata::from_file(&file)?;    // A last sanity check: if the file wasn't truncated upon opening, which is good in case something    // goes wrong during writing, not everything is lost, then after writing the file is smaller than it was    // before, it needs truncation. We do that here.    let needs_truncation = md.len() > desired_bytes;    if needs_truncation {        file.set_len(desired_bytes)?;    }    // NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.    //       revisit this once there is a bug to fix.    entry.stat = Stat::from_fs(md)?;    file.close()?;    Ok(())} /// Use `fstat` and, if needed, `fchmod` on a file descriptor to adjust whether a regular file is executable.////// See `adjust_mode_executable_bits` for the exact details of how the mode is transformed.#[cfg(unix)]fn adjust_executable_bits(file: &std::fs::File, executable: bool) -> Result<(), std::io::Error> {    use std::os::unix::fs::{MetadataExt, PermissionsExt};    let old_mode = file.metadata()?.mode();    let new_mode = adjust_mode_executable_bits(old_mode, executable);    if old_mode & 0o7777 != new_mode {        file.set_permissions(std::fs::Permissions::from_mode(new_mode))?;    }    Ok(())} /// Given the st_mode of a regular file, compute the mode with executable bits safely adjusted.////// When making a file executable, this adds executable bits for whoever has read bits already. When making a file/// non-executable, it removes all executable bits. It doesn't use the umask. Set-user-ID and set-group-ID bits are/// unset for safety. The sticky bit is also unset.////// This returns only mode bits, not file type. The return value can be used in chmod or fchmod.#[cfg(any(unix, test))]fn adjust_mode_executable_bits(mut mode: u32, executable: bool) -> u32 {    assert_eq!(mode & 0o170000, 0o100000, "bug in caller if not from a regular file");    mode &= 0o777; // Clear type, non-rwx mode bits (setuid, setgid, sticky).    if executable {        mode |= (mode & 0o444) >> 2; // Let readers also execute.    } else {        mode &= !0o111;    }    mode} #[derive(Clone, Copy)]pub(crate) enum ExecutableBitChange {    NoChange,    Set,    Remove,} #[cfg(test)]mod tests {    #[test]    fn let_readers_execute() {        let cases = [            // Common cases:            (0o100755, 0o755),            (0o100644, 0o755),            (0o100750, 0o750),            (0o100640, 0o750),            (0o100700, 0o700),            (0o100600, 0o700),            (0o100775, 0o775),            (0o100664, 0o775),            (0o100770, 0o770),            (0o100660, 0o770),            (0o100764, 0o775),            (0o100760, 0o770),            // Less common:            (0o100674, 0o775),            (0o100670, 0o770),            (0o100000, 0o000),            (0o100400, 0o500),            (0o100440, 0o550),            (0o100444, 0o555),            (0o100462, 0o572),            (0o100242, 0o252),            (0o100167, 0o177),            // With set-user-ID, set-group-ID, and sticky bits:            (0o104755, 0o755),            (0o104644, 0o755),            (0o102755, 0o755),            (0o102644, 0o755),            (0o101755, 0o755),            (0o101644, 0o755),            (0o106755, 0o755),            (0o106644, 0o755),            (0o104750, 0o750),            (0o104640, 0o750),            (0o102750, 0o750),            (0o102640, 0o750),            (0o101750, 0o750),            (0o101640, 0o750),            (0o106750, 0o750),            (0o106640, 0o750),            (0o107644, 0o755),            (0o107000, 0o000),            (0o106400, 0o500),            (0o102462, 0o572),        ];        for (st_mode, expected) in cases {            let actual = super::adjust_mode_executable_bits(st_mode, true);            assert_eq!(                actual, expected,                "{st_mode:06o} should become {expected:04o}, became {actual:04o}"            );        }    }     #[test]    fn no_one_executes() {        let cases = [            (0o100755, 0o644),            (0o100750, 0o640),            (0o100700, 0o600),            (0o100111, 0o000),            (0o100571, 0o460),            (0o107755, 0o644),        ];        for (st_mode, expected) in cases {            let actual = super::adjust_mode_executable_bits(st_mode, false);            assert_eq!(                actual, expected,                "{st_mode:06o} should become {expected:04o}, became {actual:04o}"            );        }    }     #[test]    #[should_panic]    fn let_readers_execute_panics_on_directory() {        super::adjust_mode_executable_bits(0o040644, true);    }     #[test]    #[should_panic]    fn let_readers_execute_should_panic_on_symlink() {        super::adjust_mode_executable_bits(0o120644, true);    }}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t