use std::io;
use super::super::fileio::{self, c_str};
use super::super::messages as m;
use super::super::{PK_WARN, Uz, flavour};
use super::beneath::{self, Root, Stat};
use crate::compat::host;
pub(crate) const MPN_OK: i32 = 0;
pub(crate) const MPN_INF_TRUNC: i32 = 1 << 8;
pub(crate) const MPN_INF_SKIP: i32 = 2 << 8;
pub(crate) const MPN_ERR_SKIP: i32 = 3 << 8;
pub(crate) const MPN_ERR_TOOLONG: i32 = 4 << 8;
pub(crate) const MPN_CREATED_DIR: i32 = 16 << 8;
pub(crate) const MPN_VOL_LABEL: i32 = 17 << 8;
pub(crate) const MPN_MASK: i32 = 0x7f00;
const UNIX_: u8 = 3;
#[cfg(unix)]
const S_ISGID: u32 = 0o2000;
#[derive(Default)]
pub(crate) struct Checkdir {
pub(crate) rootpath: Vec<u8>,
pub(crate) rootdir: Option<Root>,
pub(crate) cwd: Option<Root>,
pub(crate) buildpath: Option<Vec<u8>>,
pub(crate) created_dir: bool,
pub(crate) renamed_fullpath: bool,
pub(crate) create_dirs: bool,
}
impl Checkdir {
pub(crate) fn rootlen(&self) -> usize {
self.rootpath.len()
}
}
pub(crate) fn target<'a>(uz: &'a Uz, full: &'a [u8]) -> io::Result<(&'a Root, &'a [u8])> {
let ck = &uz.ck;
let rootlen = ck.rootlen();
if flavour::apple_root(uz.flavour) {
let root = ck.rootdir.as_ref().ok_or_else(enoent)?;
if rootlen > 0
&& full.len() > rootlen
&& full.starts_with(&ck.rootpath)
&& full[rootlen] == b'/'
{
return Ok((root, &full[rootlen + 1..]));
}
Err(enoent())
} else if rootlen > 0 && !ck.renamed_fullpath && full.starts_with(&ck.rootpath) {
let root = ck.rootdir.as_ref().ok_or_else(enoent)?;
Ok((root, &full[rootlen..]))
} else {
let root = ck.cwd.as_ref().ok_or_else(enoent)?;
Ok((root, full))
}
}
fn enoent() -> io::Error {
#[cfg(unix)]
{
io::Error::from_raw_os_error(libc::ENOENT)
}
#[cfg(not(unix))]
{
io::Error::from(io::ErrorKind::NotFound)
}
}
pub(crate) fn shown(uz: &mut Uz, full: &[u8]) -> Vec<u8> {
if flavour::apple_root(uz.flavour) {
let off = uz.ck.rootlen() + 1;
let raw = if off <= full.len() {
full[off..].to_vec()
} else {
c_str(uz.filename.get(off..).unwrap_or(&[])).to_vec()
};
let pfx = uz.o.exdir.clone();
return fileio::fnfilterp(uz, pfx.as_deref(), &raw);
}
fileio::fnfilter1(uz, full)
}
pub(crate) fn strerror(_uz: &Uz, e: &io::Error) -> Vec<u8> {
if beneath::is_escape(e) {
return host::strerror(&beneath::eacces());
}
host::strerror(e)
}
fn strerror_unconverted(uz: &Uz, e: &io::Error) -> Vec<u8> {
if flavour::apple_root(uz.flavour) {
return host::strerror(e);
}
strerror(uz, e)
}
pub(crate) fn lstat(uz: &Uz, full: &[u8]) -> io::Result<Stat> {
let (root, rel) = target(uz, full)?;
root.lstat(rel)
}
pub(crate) fn stat(uz: &Uz, full: &[u8]) -> io::Result<Stat> {
let (root, rel) = target(uz, full)?;
root.stat(rel)
}
fn mkdir_qtn(uz: &Uz, path: &[u8], mode: u32) -> io::Result<()> {
let (root, rel) = target(uz, path)?;
let (dir, name) = root.parent(rel)?;
match dir.mkdir(&name, mode) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists && flavour::apple_root(uz.flavour) => {}
Err(e) => return Err(e),
}
if uz.qf.is_some()
&& let Ok(d) = root.dir(rel)
&& let Err(e) = super::attrs::quarantine_dir(uz, &d)
{
let _ = dir.unlink(&name, true);
return Err(e);
}
Ok(())
}
fn mkdir_root(uz: &Uz, tmproot: &[u8], mode: u32) -> io::Result<()> {
let p = uz.host.path(tmproot);
let made = {
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
std::fs::DirBuilder::new().mode(mode).create(&p)
}
#[cfg(not(unix))]
{
let _ = mode;
std::fs::create_dir(&p)
}
};
match made {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists && flavour::apple_root(uz.flavour) => {}
Err(e) => return Err(e),
}
if uz.qf.is_some()
&& let Ok(r) = Root::open(&p)
&& let Ok(d) = r.top()
&& let Err(e) = super::attrs::quarantine_dir(uz, &d)
{
let _ = std::fs::remove_dir(&p);
return Err(e);
}
Ok(())
}
pub(crate) fn checkdir_root(uz: &mut Uz, pathcomp: &[u8]) -> i32 {
if uz.ck.rootlen() > 0 {
return MPN_OK;
}
if pathcomp.is_empty() {
return MPN_OK;
}
let mut tmproot = pathcomp.to_vec();
if tmproot.last() == Some(&b'/') {
tmproot.pop();
}
if !tmproot.is_empty() {
let p = uz.host.path(&tmproot);
let is_dir = std::fs::metadata(&p).is_ok_and(|md| md.is_dir());
if !is_dir {
if !uz.ck.create_dirs {
return MPN_INF_SKIP;
}
if let Err(e) = mkdir_root(uz, &tmproot, 0o777) {
let shown = fileio::fnfilter1(uz, &tmproot);
let err = host::strerror(&e);
uz.info_fmt(1, m::CHECKDIR_CANNOT_CREATE_ROOT, &[&shown, &err]);
return MPN_ERR_SKIP;
}
}
}
if flavour::apple_root(uz.flavour) {
let Ok(real) = std::fs::canonicalize(uz.host.path(&tmproot)) else {
return MPN_ERR_SKIP;
};
let Ok(root) = Root::open(&real) else {
return MPN_ERR_SKIP;
};
uz.ck.rootpath = real.as_os_str().as_encoded_bytes().to_vec();
uz.ck.rootdir = Some(root);
} else {
tmproot.push(b'/');
match Root::open(&uz.host.path(&tmproot)) {
Ok(root) => uz.ck.rootdir = Some(root),
Err(_) => return MPN_ERR_SKIP,
}
uz.ck.rootpath = tmproot;
}
MPN_OK
}
pub(crate) fn open_cwd(uz: &mut Uz) {
if uz.ck.cwd.is_none() {
uz.ck.cwd = Root::open(&uz.host.path(b".")).ok();
}
}
fn checkdir_init(uz: &mut Uz) {
let mut b = Vec::new();
if uz.ck.rootlen() > 0 && !uz.ck.renamed_fullpath {
b.extend_from_slice(&uz.ck.rootpath);
}
uz.ck.buildpath = Some(b);
}
fn checkdir_append_dir(uz: &mut Uz, pathcomp: &[u8]) -> i32 {
let apple = flavour::apple_root(uz.flavour);
let filnamsiz = uz.filename.len();
let mut b = uz.ck.buildpath.take().unwrap_or_default();
if apple {
b.push(b'/');
}
b.extend_from_slice(pathcomp);
let too_long = b.len() > filnamsiz - 3;
let st = if apple {
lstat(uz, &b)
} else {
match stat(uz, &b) {
Err(e) if beneath::is_escape(&e) => Ok(Stat {
mode: 0o040755,
dev: 0,
ino: 0,
mtime: 0,
}),
st => st,
}
};
match st {
Err(_) => {
if !uz.ck.create_dirs {
return MPN_INF_SKIP;
}
if too_long {
let shown = fileio::fnfilter1(uz, &b);
uz.info_fmt(1, m::CHECKDIR_TOO_LONG, &[&shown]);
return MPN_ERR_TOOLONG;
}
if let Err(e) = mkdir_qtn(uz, &b, 0o777) {
let err = strerror_unconverted(uz, &e);
let shown2 = fileio::fnfilter2(uz, &b);
let name = c_str(&uz.filename).to_vec();
let shown1 = fileio::fnfilter1(uz, &name);
uz.info_fmt(1, m::CHECKDIR_CANNOT_CREATE, &[&shown2, &err, &shown1]);
return MPN_ERR_SKIP;
}
uz.ck.created_dir = true;
}
Ok(st) if apple && st.is_symlink() => {}
Ok(st) if !st.is_dir() => {
let shown2 = fileio::fnfilter2(uz, &b);
let name = c_str(&uz.filename).to_vec();
let shown1 = fileio::fnfilter1(uz, &name);
uz.info_fmt(1, m::CHECKDIR_NOT_DIR, &[&shown2, &shown1]);
return MPN_ERR_SKIP;
}
Ok(_) => {}
}
if too_long {
let shown = fileio::fnfilter1(uz, &b);
uz.info_fmt(1, m::CHECKDIR_TOO_LONG, &[&shown]);
return MPN_ERR_TOOLONG;
}
if !apple {
b.push(b'/');
}
uz.ck.buildpath = Some(b);
MPN_OK
}
fn checkdir_append_name(uz: &mut Uz, pathcomp: &[u8]) -> i32 {
let filnamsiz = uz.filename.len();
let mut b = uz.ck.buildpath.take().unwrap_or_default();
if flavour::apple_root(uz.flavour) {
b.push(b'/');
}
for &c in pathcomp {
b.push(c);
if b.len() >= filnamsiz {
b.pop();
let name = c_str(&uz.filename).to_vec();
let shown1 = fileio::fnfilter1(uz, &name);
let shown2 = fileio::fnfilter2(uz, &b);
uz.info_fmt(0x201, m::CHECKDIR_TRUNCATING, &[&shown1, &shown2]);
uz.ck.buildpath = Some(b);
return MPN_INF_TRUNC;
}
}
uz.ck.buildpath = Some(b);
MPN_OK
}
fn checkdir_getpath(uz: &mut Uz) {
let b = uz.ck.buildpath.take().unwrap_or_default();
set_filename(uz, &b);
}
pub(crate) fn set_filename(uz: &mut Uz, s: &[u8]) {
let n = s.len().min(uz.filename.len() - 1);
uz.filename[..n].copy_from_slice(&s[..n]);
uz.filename[n] = 0;
}
pub(crate) fn filtattr(uz: &Uz, perms: u32) -> u32 {
let mut perms = perms;
if !uz.o.k_flag {
perms &= !0o7000;
}
0xffff & perms
}
pub(crate) fn mapname(
uz: &mut Uz,
pinfo: &mut super::super::extract::Info,
extra_field: Option<&[u8]>,
renamed: bool,
) -> i32 {
let mut killed_ddot = false; let mut error = MPN_OK;
if pinfo.pinfo.vollabel {
return MPN_VOL_LABEL; }
uz.ck.create_dirs = !uz.o.fflag || renamed;
uz.ck.created_dir = false;
let filename = c_str(&uz.filename).to_vec();
uz.ck.renamed_fullpath = renamed && filename.first() == Some(&b'/');
checkdir_init(uz);
let mut pathcomp: Vec<u8> = Vec::new(); let mut lastsemi: Option<usize> = None;
let acorn = flavour::acorn_ftype_nfs(uz.flavour);
let mut lastcomma: Option<usize> = None;
let mut image: Vec<u8> = Vec::new();
let cp = if uz.o.jflag {
match filename.iter().rposition(|&b| b == b'/') {
Some(i) => &filename[i + 1..],
None => &filename[..],
}
} else {
&filename[..]
};
for &workch in cp {
match workch {
b'/' => {
poke(&mut image, pathcomp.len(), 0);
if pathcomp == b"." {
pathcomp.clear();
poke(&mut image, 0, 0);
} else if uz.o.ddotflag == 0 && pathcomp == b".." {
pathcomp.clear();
poke(&mut image, 0, 0);
killed_ddot = true; }
if !pathcomp.is_empty() {
error = checkdir_append_dir(uz, &pathcomp);
if error & MPN_MASK > MPN_INF_TRUNC {
return error;
}
}
pathcomp.clear(); lastsemi = None; }
b';' => {
lastsemi = Some(pathcomp.len());
put(&mut pathcomp, &mut image, b';');
}
b',' if acorn => {
lastcomma = Some(pathcomp.len());
put(&mut pathcomp, &mut image, b',');
}
_ => {
if uz.o.cflxflag != 0
|| fileio::is_print(uz, workch)
|| (128..=254).contains(&workch)
{
put(&mut pathcomp, &mut image, workch);
}
}
}
}
if killed_ddot && uz.o.qflag == 0 {
let shown = fileio::fnfilter1(uz, &filename);
uz.info_fmt(0, m::SKIPPED_DDOT, &[&shown]);
if error & !MPN_MASK == 0 {
error = (error & MPN_MASK) | PK_WARN;
}
}
if filename.last() == Some(&b'/') {
checkdir_getpath(uz);
if uz.ck.created_dir {
let full = c_str(&uz.filename).to_vec();
if uz.o.qflag == 0 {
let shown = shown(uz, &full);
uz.info_fmt(0, m::CREATING, &[&shown]);
}
pinfo.file_attr = filtattr(uz, pinfo.file_attr);
if pinfo.pinfo.hostnum != UNIX_ || !(uz.o.x_flag != 0 || uz.o.k_flag) {
match std::fs::metadata(uz.host.path(&full)) {
#[cfg(unix)]
Ok(st) => {
use std::os::unix::fs::PermissionsExt;
pinfo.file_attr |= st.permissions().mode() & S_ISGID;
}
#[cfg(not(unix))]
Ok(_) => {}
Err(e) => perror(uz, m::PERROR_DIR_ATTRS, &e),
}
}
if let Err(e) = super::attrs::chmod_dir(uz, &full, pinfo.file_attr | 0o700) {
perror(uz, m::PERROR_CHMOD_DIR, &e);
}
return (error & !MPN_MASK) | MPN_CREATED_DIR;
}
return (error & !MPN_MASK) | MPN_INF_SKIP;
}
poke(&mut image, pathcomp.len(), 0);
if !uz.o.v_flag
&& let Some(semi) = lastsemi
&& pathcomp[semi + 1..].iter().all(u8::is_ascii_digit)
{
pathcomp.truncate(semi);
poke(&mut image, semi, 0);
}
if pathcomp == b"." {
pathcomp = b"_".to_vec();
poke(&mut image, 0, b'_');
} else if pathcomp == b".." {
pathcomp = b"__".to_vec();
for (i, &c) in b"__\0".iter().enumerate() {
poke(&mut image, i, c);
}
}
if acorn
&& uz.o.acorn_nfs_ext
&& let Some(ef_spark) = extra_field.and_then(get_riscos_exfield)
{
let ft = fileio::makelong(&ef_spark[8..]);
if let Some(comma) = lastcomma {
let at = |i: usize| image.get(i).copied().unwrap_or(0);
let mut pp = comma + 1;
while at(pp).is_ascii_hexdigit() {
pp += 1;
}
if pp == comma + 4 && at(pp) == 0 {
pathcomp.truncate(comma);
poke(&mut image, comma, 0);
}
}
let ft = if ft & (1 << 31) == 0 { 0x000F_FD00 } else { ft };
pathcomp.extend_from_slice(format!(",{:03x}", (ft >> 8) & 0xFFF).as_bytes());
}
if pathcomp.is_empty() {
let shown = fileio::fnfilter1(uz, &filename);
uz.info_fmt(1, m::MAPNAME_FAILED, &[&shown]);
return (error & !MPN_MASK) | MPN_ERR_SKIP;
}
checkdir_append_name(uz, &pathcomp); checkdir_getpath(uz);
error
}
fn put(pathcomp: &mut Vec<u8>, image: &mut Vec<u8>, c: u8) {
poke(image, pathcomp.len(), c);
pathcomp.push(c);
}
fn poke(image: &mut Vec<u8>, i: usize, c: u8) {
if image.len() <= i {
image.resize(i + 1, 0);
}
image[i] = c;
}
const SPARKID_2: u32 = 0x3043_5241;
const EF_SPARK: u16 = 0x4341;
fn get_riscos_exfield(ef: &[u8]) -> Option<&[u8]> {
let mut ef = ef;
while ef.len() >= 4 {
let eb_id = fileio::makeword(ef);
let eb_len = usize::from(fileio::makeword(&ef[2..]));
if eb_len > ef.len() - 4 {
break;
}
if eb_id == EF_SPARK
&& (eb_len == 24 || eb_len == 20)
&& fileio::makelong(&ef[4..]) == SPARKID_2
{
return Some(&ef[..4 + eb_len]);
}
ef = &ef[4 + eb_len..];
}
None
}
pub(crate) fn perror(uz: &mut Uz, s: &str, e: &io::Error) {
let mut v = s.as_bytes().to_vec();
v.extend_from_slice(b": ");
v.extend_from_slice(&strerror(uz, e));
v.push(b'\n');
let _ = uz.err.write_all(&v);
let _ = uz.err.flush();
}