portable-network-archive 0.34.0

Portable-Network-Archive cli
Documentation
use crate::{
    cli::{PasswordArgs, SolidEntriesTransformStrategy, SolidEntriesTransformStrategyArgs},
    command::{
        Command, ask_password,
        core::{
            SplitArchiveReader, TransformStrategyKeepSolid, TransformStrategyUnSolid,
            collect_split_archives,
        },
    },
    utils::{
        GlobPatterns, PathPartExt,
        env::NamedTempFile,
        fs::{Group, User},
    },
};
use clap::{ArgGroup, Parser, ValueHint};
use pna::NormalEntry;
use std::{io, ops::Not, path::PathBuf, str::FromStr};

#[derive(Parser, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[command(
    group(ArgGroup::new("lookup").args(["owner_lookup", "no_owner_lookup"])),
)]
pub(crate) struct ChownCommand {
    #[arg(short = 'f', long = "file", value_hint = ValueHint::FilePath)]
    archive: PathBuf,
    #[arg(help = "owner[:group]|:group")]
    owner: RawOwnership,
    #[arg(long, help = "force numeric owner and group IDs (no name resolution)")]
    numeric_owner: bool,
    #[arg(long, help = "resolve user and group (default)")]
    owner_lookup: bool,
    #[arg(long, help = "do not resolve user and group")]
    no_owner_lookup: bool,
    #[arg(value_hint = ValueHint::AnyPath)]
    files: Vec<String>,
    #[command(flatten)]
    transform_strategy: SolidEntriesTransformStrategyArgs,
    #[command(flatten)]
    password: PasswordArgs,
}

impl Command for ChownCommand {
    #[inline]
    fn execute(self, _ctx: &crate::cli::GlobalContext) -> anyhow::Result<()> {
        archive_chown(self)
    }
}

#[hooq::hooq(anyhow)]
fn archive_chown(args: ChownCommand) -> anyhow::Result<()> {
    let password = ask_password(args.password)?;
    if args.files.is_empty() {
        return Ok(());
    }
    let mut globs = GlobPatterns::new(args.files.iter().map(|p| p.as_ref()))?;

    let owner = args
        .owner
        .lookup_platform_owner(args.numeric_owner, !args.no_owner_lookup)?;

    let mut source = SplitArchiveReader::new(collect_split_archives(&args.archive)?)?;

    let output_path = args.archive.remove_part();
    let mut temp_file =
        NamedTempFile::new(|| output_path.parent().unwrap_or_else(|| ".".as_ref()))?;

    match args.transform_strategy.strategy() {
        SolidEntriesTransformStrategy::UnSolid => source.transform_entries(
            temp_file.as_file_mut(),
            password.as_deref(),
            #[hooq::skip_all]
            |entry| {
                let entry = entry?;
                if globs.matches_any(entry.name()) {
                    Ok(Some(transform_entry(entry, &owner)))
                } else {
                    Ok(Some(entry))
                }
            },
            TransformStrategyUnSolid,
        ),
        SolidEntriesTransformStrategy::KeepSolid => source.transform_entries(
            temp_file.as_file_mut(),
            password.as_deref(),
            #[hooq::skip_all]
            |entry| {
                let entry = entry?;
                if globs.matches_any(entry.name()) {
                    Ok(Some(transform_entry(entry, &owner)))
                } else {
                    Ok(Some(entry))
                }
            },
            TransformStrategyKeepSolid,
        ),
    }?;

    drop(source);

    temp_file.persist(output_path)?;

    globs.ensure_all_matched()?;
    Ok(())
}

#[inline]
#[allow(deprecated)]
fn transform_entry<T>(entry: NormalEntry<T>, owner: &Ownership) -> NormalEntry<T> {
    let metadata = entry.metadata().clone();
    let own = crate::ext::ResolvedOwnership::from_metadata(&metadata);
    let (uid, uname): (Option<u64>, String) = match owner.user.as_ref() {
        Some(OwnerSpecifier::Name(uname)) => (Some(u64::MAX), uname.clone()),
        Some(OwnerSpecifier::ID(uid)) => (Some(*uid), String::new()),
        Some(OwnerSpecifier::System(user)) => (
            Some(user.uid().unwrap_or(u64::MAX)),
            user.name().unwrap_or_default().into(),
        ),
        None => (own.uid, own.uname.clone().unwrap_or_default()),
    };
    let (gid, gname): (Option<u64>, String) = match owner.group.as_ref() {
        Some(OwnerSpecifier::Name(gname)) => (Some(u64::MAX), gname.clone()),
        Some(OwnerSpecifier::ID(gid)) => (Some(*gid), String::new()),
        Some(OwnerSpecifier::System(group)) => (
            Some(group.gid().unwrap_or(u64::MAX)),
            group.name().unwrap_or_default().into(),
        ),
        None => (own.gid, own.gname.clone().unwrap_or_default()),
    };
    let metadata =
        metadata
            .with_permission(None)
            .with_owner_uid(uid.map(pna::OwnerUid::from))
            .with_owner_gid(gid.map(pna::OwnerGid::from))
            .with_owner_user_name(crate::command::core::permission::owner_name_opt(&uname))
            .with_owner_group_name(crate::command::core::permission::owner_group_name_opt(
                &gname,
            ))
            .with_permission_mode(own.mode.map(pna::PermissionMode::from))
            .with_owner_user_sid(
                own.user_sid.clone().map(|s| {
                    pna::OwnerUserSid::new(s).expect("rescued sid within owner-facet bound")
                }),
            )
            .with_owner_group_sid(own.group_sid.clone().map(|s| {
                pna::OwnerGroupSid::new(s).expect("rescued sid within owner-facet bound")
            }));
    entry.with_metadata(metadata)
}

pub(crate) enum OwnerSpecifier<T> {
    Name(String),
    ID(u64),
    System(T),
}

pub(crate) struct Ownership {
    user: Option<OwnerSpecifier<User>>,
    group: Option<OwnerSpecifier<Group>>,
}

#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) struct RawOwnership {
    user: Option<String>,
    group: Option<String>,
    use_login_group: bool,
}

impl RawOwnership {
    #[inline]
    fn lookup_platform_owner(self, numeric_owner: bool, lookup: bool) -> io::Result<Ownership> {
        if self.use_login_group && !lookup {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "cannot use 'user:' format with --no-owner-lookup",
            ));
        }

        let user = match self.user {
            Some(user) if lookup => Some(OwnerSpecifier::System(if numeric_owner {
                User::from_uid(
                    user.parse()
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?,
                )?
            } else {
                User::from_name(&user)?
            })),
            Some(user) => Some(if numeric_owner {
                OwnerSpecifier::ID(
                    user.parse()
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?,
                )
            } else {
                OwnerSpecifier::Name(user)
            }),
            None => None,
        };

        let group = match self.group {
            _ if self.use_login_group => match &user {
                Some(OwnerSpecifier::System(u)) => {
                    let gid = u.primary_gid().ok_or_else(|| {
                        io::Error::new(
                            io::ErrorKind::Unsupported,
                            "cannot get user's primary group",
                        )
                    })?;
                    Some(OwnerSpecifier::System(Group::from_gid(gid)?))
                }
                _ => None,
            },
            Some(group) if lookup => Some(OwnerSpecifier::System(if numeric_owner {
                Group::from_gid(
                    group
                        .parse()
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?,
                )?
            } else {
                Group::from_name(&group)?
            })),
            Some(group) => Some(if numeric_owner {
                OwnerSpecifier::ID(
                    group
                        .parse()
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?,
                )
            } else {
                OwnerSpecifier::Name(group)
            }),
            None => None,
        };

        Ok(Ownership { user, group })
    }
}

impl FromStr for RawOwnership {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() || s == ":" {
            return Err("owner must not be empty".into());
        }
        let (user, group, use_login_group) = if let Some((user, group)) = s.split_once(':') {
            (
                user.is_empty().not().then(|| user.into()),
                group.is_empty().not().then(|| group.into()),
                !user.is_empty() && group.is_empty(),
            )
        } else {
            (Some(s.into()), None, false)
        };
        Ok(Self {
            user,
            group,
            use_login_group,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn owner_from_str_user() {
        assert_eq!(
            RawOwnership::from_str("user").unwrap(),
            RawOwnership {
                user: Some("user".into()),
                group: None,
                use_login_group: false,
            }
        );
    }

    #[test]
    fn owner_from_str_user_login_group() {
        assert_eq!(
            RawOwnership::from_str("user:").unwrap(),
            RawOwnership {
                user: Some("user".into()),
                group: None,
                use_login_group: true,
            }
        );
    }

    #[test]
    fn group() {
        assert_eq!(
            RawOwnership::from_str(":group").unwrap(),
            RawOwnership {
                user: None,
                group: Some("group".into()),
                use_login_group: false,
            }
        );
    }

    #[test]
    fn user_group() {
        assert_eq!(
            RawOwnership::from_str("user:group").unwrap(),
            RawOwnership {
                user: Some("user".into()),
                group: Some("group".into()),
                use_login_group: false,
            }
        );
    }
}