Sebastian Thielmake fmt5d2b5d03 years agoHistory
166 lines · 6.3 KB · Rust
RawDownload
use std::{fs::OpenOptions, io::Write, path::Path}; use bstr::BStr;use gix_hash::oid;use gix_index::{entry::Stat, Entry};use io_close::Close; use crate::Cache; pub struct Context<'a, Find> {    pub find: &'a mut Find,    pub path_cache: &'a mut Cache,    pub buf: &'a mut Vec<u8>,} #[cfg_attr(not(unix), allow(unused_variables))]pub fn checkout<Find, E>(    entry: &mut Entry,    entry_path: &BStr,    Context { find, path_cache, buf }: Context<'_, Find>,    crate::checkout::Options {        fs: gix_fs::Capabilities {            symlink,            executable_bit,            ..        },        destination_is_initially_empty,        overwrite_existing,        ..    }: crate::checkout::Options,) -> Result<usize, crate::checkout::Error<E>>where    Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,    E: std::error::Error + Send + Sync + 'static,{    let dest_relative = gix_path::try_from_bstr(entry_path).map_err(|_| crate::checkout::Error::IllformedUtf8 {        path: entry_path.to_owned(),    })?;    let is_dir = Some(entry.mode == gix_index::entry::Mode::COMMIT || entry.mode == gix_index::entry::Mode::DIR);    let dest = path_cache.at_path(dest_relative, is_dir, &mut *find)?.path();     let object_size = match entry.mode {        gix_index::entry::Mode::FILE | gix_index::entry::Mode::FILE_EXECUTABLE => {            let obj = find(&entry.id, buf).map_err(|err| crate::checkout::Error::Find {                err,                oid: entry.id,                path: dest.to_path_buf(),            })?;             #[cfg_attr(not(unix), allow(unused_mut))]            let mut options = open_options(dest, destination_is_initially_empty, overwrite_existing);            let needs_executable_bit = executable_bit && entry.mode == gix_index::entry::Mode::FILE_EXECUTABLE;            #[cfg(unix)]            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);            }             let mut file = try_write_or_unlink(dest, overwrite_existing, |p| options.open(p))?;            file.write_all(obj.data)?;             // For possibly existing, overwritten files, we must change the file mode explicitly.            #[cfg(unix)]            if needs_executable_bit && !destination_is_initially_empty {                use std::os::unix::fs::PermissionsExt;                let mut perm = std::fs::symlink_metadata(dest)?.permissions();                perm.set_mode(0o777);                std::fs::set_permissions(dest, perm)?;            }            // 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(&file.metadata()?)?;            file.close()?;            obj.data.len()        }        gix_index::entry::Mode::SYMLINK => {            let obj = find(&entry.id, buf).map_err(|err| crate::checkout::Error::Find {                err,                oid: entry.id,                path: dest.to_path_buf(),            })?;            let symlink_destination = gix_path::try_from_byte_slice(obj.data)                .map_err(|_| crate::checkout::Error::IllformedUtf8 { path: obj.data.into() })?;             if symlink {                try_write_or_unlink(dest, overwrite_existing, |p| {                    gix_fs::symlink::create(symlink_destination, p)                })?;            } else {                let mut file = try_write_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(&std::fs::symlink_metadata(dest)?)?;            obj.data.len()        }        gix_index::entry::Mode::DIR => todo!(),        gix_index::entry::Mode::COMMIT => todo!(),        _ => unreachable!(),    };    Ok(object_size)} /// 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_write_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) -> 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);    options}

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