use std::ffi::OsStr;
use std::fs::{self, File};
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{symlink, OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use blake3::Hash;
use pkgar_core::{Mode, PackageSrc};
use crate::ext::{copy_and_hash, EntryExt, PackageSrcExt};
use crate::{Error, ErrorKind, ResultExt, READ_WRITE_HASH_BUF_SIZE};
fn file_exists(path: impl AsRef<Path>) -> Result<bool, Error> {
if let Err(err) = fs::metadata(&path) {
if err.kind() == io::ErrorKind::NotFound {
Ok(false)
} else {
Err(Error::from(err).chain_err(|| path.as_ref()))
}
} else {
Ok(true)
}
}
fn temp_path(target_path: impl AsRef<Path>, entry_hash: Hash) -> Result<PathBuf, Error> {
let target_path = target_path.as_ref();
let hash_path = format!(".pkgar.{}", entry_hash.to_hex());
let tmp_name = if let Some(filename) = target_path.file_name() {
let name_path = format!(".pkgar.{}", Path::new(filename).display());
if file_exists(&name_path)? {
eprintln!("warn: temporary path already exists at {}", name_path);
hash_path
} else {
name_path
}
} else {
hash_path
};
let parent_dir = target_path
.parent()
.ok_or(ErrorKind::InvalidPathComponent(PathBuf::from("/")))?;
fs::create_dir_all(parent_dir).chain_err(|| parent_dir)?;
Ok(parent_dir.join(tmp_name))
}
enum Action {
Rename(PathBuf, PathBuf),
Remove(PathBuf),
}
impl Action {
fn commit(&self) -> Result<(), Error> {
match self {
Action::Rename(tmp, target) => fs::rename(&tmp, target).chain_err(|| tmp),
Action::Remove(target) => fs::remove_file(&target).chain_err(|| target),
}
}
fn abort(&self) -> Result<(), Error> {
match self {
Action::Rename(tmp, _) => fs::remove_file(&tmp).chain_err(|| tmp),
Action::Remove(_) => Ok(()),
}
}
}
pub struct Transaction {
actions: Vec<Action>,
}
impl Transaction {
pub fn install<Pkg>(src: &mut Pkg, base_dir: impl AsRef<Path>) -> Result<Transaction, Error>
where
Pkg: PackageSrc<Err = Error> + PackageSrcExt,
{
let mut buf = vec![0; READ_WRITE_HASH_BUF_SIZE];
let entries = src.read_entries()?;
let mut actions = Vec::with_capacity(entries.len());
for entry in entries {
let relative_path = entry.check_path().chain_err(|| src.path())?;
let target_path = base_dir.as_ref().join(relative_path);
assert!(
target_path.starts_with(&base_dir),
"target path was not in the base path"
);
let tmp_path = temp_path(&target_path, entry.blake3())?;
let mode = entry
.mode()
.map_err(Error::from)
.chain_err(|| src.path())
.chain_err(|| ErrorKind::Entry(entry))?;
let (entry_data_size, entry_data_hash) = match mode.kind() {
Mode::FILE => {
let mut tmp_file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(mode.perm().bits())
.open(&tmp_path)
.chain_err(|| tmp_path.as_path())?;
let (size, hash) = copy_and_hash(src.entry_reader(entry), &mut tmp_file, &mut buf)
.chain_err(|| &tmp_path)
.chain_err(|| {
format!("Copying entry to tempfile: '{}'", relative_path.display())
})?;
if mode.perm().bits() & 0o6000 == 0o6000 {
let metadata = tmp_file.metadata()?;
let mut permissions = metadata.permissions();
permissions.set_mode(metadata.permissions().mode() | 0o6000);
fs::set_permissions(&tmp_path, permissions)?;
}
(size, hash)
}
Mode::SYMLINK => {
let mut data = Vec::new();
let (size, hash) = copy_and_hash(src.entry_reader(entry), &mut data, &mut buf)
.chain_err(|| &tmp_path)
.chain_err(|| {
format!("Copying entry to tempfile: '{}'", relative_path.display())
})?;
let sym_target = Path::new(OsStr::from_bytes(&data));
symlink(sym_target, &tmp_path)
.chain_err(|| sym_target)
.chain_err(|| format!("Symlinking to {}", tmp_path.display()))?;
(size, hash)
}
_ => {
return Err(Error::from(pkgar_core::Error::InvalidMode(mode.bits())))
.chain_err(|| ErrorKind::Entry(entry))
.chain_err(|| src.path());
}
};
entry
.verify(entry_data_hash, entry_data_size)
.chain_err(|| src.path())?;
actions.push(Action::Rename(tmp_path, target_path))
}
Ok(Transaction { actions })
}
pub fn replace<Pkg>(
old: &mut Pkg,
new: &mut Pkg,
base_dir: impl AsRef<Path>,
) -> Result<Transaction, Error>
where
Pkg: PackageSrc<Err = Error> + PackageSrcExt,
{
let old_entries = old.read_entries()?;
let new_entries = new.read_entries()?;
let mut actions = old_entries
.iter()
.filter(|old_e| {
new_entries
.iter()
.find(|new_e| new_e.blake3() == old_e.blake3())
.is_none()
})
.map(|e| {
let target_path = base_dir.as_ref().join(e.check_path()?);
Ok(Action::Remove(target_path))
})
.collect::<Result<Vec<Action>, Error>>()?;
let mut trans = Transaction::install(new, base_dir)?;
trans.actions.append(&mut actions);
Ok(trans)
}
pub fn remove<Pkg>(pkg: &mut Pkg, base_dir: impl AsRef<Path>) -> Result<Transaction, Error>
where
Pkg: PackageSrc<Err = Error>,
{
let mut buf = vec![0; READ_WRITE_HASH_BUF_SIZE];
let entries = pkg.read_entries()?;
let mut actions = Vec::with_capacity(entries.len());
for entry in entries {
let relative_path = entry.check_path()?;
let target_path = base_dir.as_ref().join(relative_path);
assert!(
target_path.starts_with(&base_dir),
"target path was not in the base path"
);
let candidate = File::open(&target_path).chain_err(|| &target_path)?;
copy_and_hash(candidate, io::sink(), &mut buf)
.chain_err(|| &target_path)
.chain_err(|| format!("Hashing file for entry: '{}'", relative_path.display()))?;
actions.push(Action::Remove(target_path));
}
Ok(Transaction { actions })
}
pub fn commit(&mut self) -> Result<usize, Error> {
let mut count = 0;
while let Some(action) = self.actions.pop() {
if let Err(err) = action.commit() {
self.actions.push(action);
return Err(err.chain_err(|| ErrorKind::FailedCommit(count, self.actions.len())));
}
count += 1;
}
Ok(count)
}
pub fn abort(&mut self) -> Result<usize, Error> {
let mut count = 0;
let mut last_failed = false;
while let Some(action) = self.actions.pop() {
if let Err(err) = action.abort() {
self.actions.insert(0, action);
if last_failed {
return Err(err
.chain_err(|| ErrorKind::FailedCommit(count, self.actions.len()))
.chain_err(|| "Abort triggered"));
} else {
last_failed = true;
}
}
count += 1;
}
Ok(count)
}
}