use std::collections::{BTreeMap, HashMap};
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};
use std::path::{Path, PathBuf};
use blake3::Hash;
use pkgar_core::{Entry, Mode, PackageSrc};
use crate::ext::{copy_and_hash, EntryExt, PackageSrcExt};
use crate::{wrap_io_err, Error, READ_WRITE_HASH_BUF_SIZE};
fn file_exists(path: impl AsRef<Path>) -> Result<bool, Error> {
let path = path.as_ref();
if let Err(err) = fs::symlink_metadata(path) {
if err.kind() == io::ErrorKind::NotFound {
Ok(false)
} else {
Err(Error::Io {
source: err,
path: Some(path.to_path_buf()),
context: "Checking file",
})
}
} 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 parent_dir = target_path
.parent()
.ok_or_else(|| Error::InvalidPathComponent {
invalid: PathBuf::from("/"),
path: target_path.to_path_buf(),
entry: None,
})?;
let tmp_name = if let Some(filename) = target_path.file_name() {
let name_path = format!(".pkgar.{}", Path::new(filename).display());
if file_exists(parent_dir.join(&name_path))? {
hash_path
} else {
name_path
}
} else {
hash_path
};
fs::create_dir_all(parent_dir)
.map_err(wrap_io_err!(parent_dir.to_path_buf(), "Creating dir"))?;
Ok(parent_dir.join(tmp_name))
}
#[derive(Clone, Debug)]
pub enum Action {
Rename(PathBuf, PathBuf),
Remove(PathBuf),
}
impl Action {
fn commit(&self) -> Result<(), Error> {
match self {
Action::Rename(tmp, target) => {
fs::rename(tmp, target).map_err(wrap_io_err!(tmp.to_path_buf(), "Renaming file"))
}
Action::Remove(target) => {
fs::remove_file(target).map_err(wrap_io_err!(target.to_path_buf(), "Removing file"))
}
}
}
fn abort(&self) -> Result<(), Error> {
match self {
Action::Rename(tmp, _) => {
fs::remove_file(tmp).map_err(wrap_io_err!(tmp.to_path_buf(), "Removing tempfile"))
}
Action::Remove(_) => Ok(()),
}
}
pub fn target_file(&self) -> &Path {
match self {
Action::Rename(_, path) => path.as_path(),
Action::Remove(path) => path.as_path(),
}
}
}
pub struct Transaction {
actions: Vec<Action>,
committed: usize,
}
impl Transaction {
fn new(actions: Vec<Action>) -> Self {
Self {
actions,
committed: 0,
}
}
pub fn install<Pkg>(src: &mut Pkg, base_dir: impl AsRef<Path>) -> Result<Self, Error>
where
Pkg: PackageSrc<Err = Error> + PackageSrcExt<File>,
{
let entries = src.read_entries()?;
Self::install_with_entries(src, entries, base_dir, true)
}
pub fn install_with_entries<Pkg>(
src: &mut Pkg,
entries: Vec<Entry>,
base_dir: impl AsRef<Path>,
skip_local_check: bool,
) -> Result<Self, Error>
where
Pkg: PackageSrc<Err = Error> + PackageSrcExt<File>,
{
let mut buf = vec![0; READ_WRITE_HASH_BUF_SIZE];
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 tmp_path = temp_path(&target_path, entry.blake3())?;
let mode = entry.mode().map_err(Error::from)?;
let mut data_reader = src.data_reader(&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)
.map_err(wrap_io_err!(tmp_path, "Opening tempfile"))?;
let (size, hash) = copy_and_hash(&mut data_reader, &mut tmp_file, &mut buf)
.map_err(wrap_io_err!(tmp_path, "Copying entry to tempfile"))?;
actions.push(Action::Rename(tmp_path, target_path));
(size, hash)
}
Mode::SYMLINK => {
let mut data = Vec::new();
let (size, hash) = copy_and_hash(&mut data_reader, &mut data, &mut buf)
.map_err(wrap_io_err!(tmp_path, "Copying entry to tempfile"))?;
let sym_target = Path::new(OsStr::from_bytes(&data));
let mut retried = false;
loop {
match symlink(sym_target, &tmp_path)
.map_err(wrap_io_err!(tmp_path, "Symlinking to tmp"))
{
Ok(_) => break,
Err(e) if retried => return Err(e),
Err(_) => {
fs::remove_file(&tmp_path)
.map_err(wrap_io_err!(tmp_path, "Unlinking old symlink tmp"))?;
retried = true
}
}
}
actions.push(Action::Rename(tmp_path, target_path));
(size, hash)
}
_ => {
return Err(Error::from(pkgar_core::Error::InvalidMode(mode.bits())));
}
};
entry.verify(entry_data_hash, entry_data_size, &data_reader)?;
data_reader.finish(src)?;
}
if !skip_local_check {
let mut allowed_install_actions = Vec::with_capacity(actions.len());
let mut buf = vec![0; READ_WRITE_HASH_BUF_SIZE];
for (i, action) in actions.into_iter().enumerate() {
let target_path = action.target_file();
if !target_path.is_file() {
allowed_install_actions.push(action);
continue;
}
let mut candidate = File::open(&target_path)
.map_err(wrap_io_err!(target_path, "Opening candidate"))?;
let (_, entry_data_hash) = copy_and_hash(&mut candidate, &mut io::sink(), &mut buf)
.map_err(wrap_io_err!(target_path, "Hashing file for entry"))?;
if entry_data_hash == entries[i].blake3() {
allowed_install_actions.push(action);
} else {
action.abort()?;
}
}
actions = allowed_install_actions;
}
Ok(Transaction::new(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<File>,
{
let old_entries = old.read_entries()?;
let new_entries = new.read_entries()?;
Self::replace_with_entries(old_entries, new_entries, new, base_dir, false)
}
pub fn replace_with_entries<Pkg>(
old_entries: Vec<Entry>,
new_entries: Vec<Entry>,
new: &mut Pkg,
base_dir: impl AsRef<Path>,
skip_local_check: bool,
) -> Result<Transaction, Error>
where
Pkg: PackageSrc<Err = Error> + PackageSrcExt<File>,
{
let mut old_map = HashMap::with_capacity(old_entries.len());
for entry in old_entries {
old_map.insert(entry.check_path()?.to_path_buf(), entry);
}
let mut entries_to_install = Vec::new();
for entry in new_entries {
let path = entry.check_path()?;
old_map.remove(path);
match old_map.get(path) {
Some(old_hash) if old_hash.blake3() == entry.blake3() => {
continue;
}
_ => {
entries_to_install.push(entry);
}
}
}
let mut entries_to_remove = Vec::new();
for old_e in old_map.into_values() {
entries_to_remove.push(old_e);
}
let mut trans = Self::install_with_entries(
new,
entries_to_install.clone(),
&base_dir,
skip_local_check,
)?;
let remove_trans =
Self::remove_with_entries(entries_to_remove, &base_dir, skip_local_check)?;
trans.actions.extend(remove_trans.actions);
Ok(trans)
}
pub fn remove<Pkg>(src: &mut Pkg, base_dir: impl AsRef<Path>) -> Result<Self, Error>
where
Pkg: PackageSrc<Err = Error>,
{
let entries = src.read_entries()?;
Self::remove_with_entries(entries, base_dir, false)
}
pub fn remove_with_entries(
entries: Vec<Entry>,
base_dir: impl AsRef<Path>,
skip_local_check: bool,
) -> Result<Self, Error> {
let mut buf = vec![0; READ_WRITE_HASH_BUF_SIZE];
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 mut candidate = File::open(&target_path)
.map_err(wrap_io_err!(target_path.clone(), "Opening candidate"))?;
let (_, entry_data_hash) = copy_and_hash(&mut candidate, &mut io::sink(), &mut buf)
.map_err(wrap_io_err!(target_path.clone(), "Hashing file for entry"))?;
if skip_local_check || entry_data_hash == entry.blake3() {
actions.push(Action::Remove(target_path));
}
}
Ok(Transaction::new(actions))
}
pub fn commit(&mut self) -> Result<usize, Error> {
self.reset_committed();
while self.actions.len() > 0 {
self.commit_one()?;
}
Ok(self.committed)
}
pub fn commit_one(&mut self) -> Result<usize, Error> {
if let Some(action) = self.actions.pop() {
if let Err(err) = action.commit() {
self.actions.push(action);
return Err(Error::FailedCommit {
source: Box::new(err),
changed: self.committed,
remaining: self.actions.len(),
});
}
self.committed += 1;
}
Ok(self.committed)
}
pub fn abort(&mut self) -> Result<usize, Error> {
let mut last_failed = false;
self.reset_committed();
while self.actions.len() > 0 {
if let Err(err) = self.abort_one() {
if last_failed {
return Err(err);
} else {
last_failed = true;
}
}
}
Ok(self.committed)
}
pub fn abort_one(&mut self) -> Result<usize, Error> {
if let Some(action) = self.actions.pop() {
if let Err(err) = action.abort() {
self.actions.insert(0, action);
return Err(Error::FailedCommit {
source: Box::new(err),
changed: self.committed,
remaining: self.actions.len(),
});
}
self.committed += 1;
}
Ok(self.committed)
}
pub fn pending_commit(&self) -> usize {
self.actions.len()
}
pub fn total_committed(&self) -> usize {
self.committed
}
pub fn reset_committed(&mut self) {
self.committed = 0;
}
pub fn get_actions(&self) -> &Vec<Action> {
&self.actions
}
}
pub struct MergedTransaction {
actions: Vec<Action>,
path_map: BTreeMap<PathBuf, Option<String>>,
possible_conflicts: Vec<TransactionConflict>,
}
impl MergedTransaction {
pub fn new() -> Self {
MergedTransaction {
actions: Vec::new(),
path_map: BTreeMap::new(),
possible_conflicts: Vec::new(),
}
}
fn push_action<Pkg>(&mut self, action: Action, src: Option<&Pkg>)
where
Pkg: PackageSrc<Err = Error> + PackageSrcExt<File>,
{
let action_key = action.target_file();
match self.path_map.entry(action_key.to_path_buf()) {
std::collections::btree_map::Entry::Vacant(vacant_entry) => {
vacant_entry.insert(src.map(|s| s.path().to_string()));
self.actions.push(action);
}
std::collections::btree_map::Entry::Occupied(occupied_entry) => {
self.possible_conflicts.push(TransactionConflict {
conflicted_path: action_key.to_path_buf(),
former_src: occupied_entry.get().clone(),
newer_src: src.map(|s| s.path().to_string()),
});
}
}
}
pub fn merge<Pkg>(&mut self, newer: Transaction, src: Option<&Pkg>)
where
Pkg: PackageSrc<Err = Error> + PackageSrcExt<File>,
{
for action in newer.actions {
self.push_action(action, src);
}
}
pub fn get_possible_conflicts(&self) -> &Vec<TransactionConflict> {
&self.possible_conflicts
}
pub fn get_actions(&self) -> &Vec<Action> {
&self.actions
}
pub fn into_transaction(self) -> Transaction {
Transaction::new(self.actions)
}
}
pub struct TransactionConflict {
pub conflicted_path: PathBuf,
pub former_src: Option<String>,
pub newer_src: Option<String>,
}