portable-network-archive 0.32.2

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]
fn transform_entry<T>(entry: NormalEntry<T>, owner: &Ownership) -> NormalEntry<T> {
    let metadata = entry.metadata().clone();
    let permission = metadata.permission().map(|p| {
        let user = owner.user.as_ref().map(|it| match it {
            OwnerSpecifier::Name(uname) => (u64::MAX, uname.into()),
            OwnerSpecifier::ID(uid) => (*uid, String::new()),
            OwnerSpecifier::System(user) => (
                user.uid().unwrap_or(u64::MAX),
                user.name().unwrap_or_default().into(),
            ),
        });
        let (uid, uname) = user.unwrap_or_else(|| (p.uid(), p.uname().into()));

        let group = owner.group.as_ref().map(|it| match it {
            OwnerSpecifier::Name(gname) => (u64::MAX, gname.into()),
            OwnerSpecifier::ID(gid) => (*gid, String::new()),
            OwnerSpecifier::System(group) => (
                group.gid().unwrap_or(u64::MAX),
                group.name().unwrap_or_default().into(),
            ),
        });
        let (gid, gname) = group.unwrap_or_else(|| (p.gid(), p.gname().into()));
        pna::Permission::new(uid, uname, gid, gname, p.permissions())
    });
    entry.with_metadata(metadata.with_permission(permission))
}

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,
            }
        );
    }
}