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