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::{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::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 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_else(|| Error::InvalidPathComponent {
invalid: PathBuf::from("/"),
path: target_path.to_path_buf(),
entry: None,
})?;
fs::create_dir_all(parent_dir).map_err(|source| Error::Io {
source,
path: Some(parent_dir.to_path_buf()),
context: "Creating 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).map_err(|source| Error::Io {
source,
path: Some(tmp.to_path_buf()),
context: "Renaming file",
}),
Action::Remove(target) => fs::remove_file(target).map_err(|source| Error::Io {
source,
path: Some(target.to_path_buf()),
context: "Removing file",
}),
}
}
fn abort(&self) -> Result<(), Error> {
match self {
Action::Rename(tmp, _) => fs::remove_file(tmp).map_err(|source| Error::Io {
source,
path: Some(tmp.to_path_buf()),
context: "Removing tempfile",
}),
Action::Remove(_) => Ok(()),
}
}
}
pub struct Transaction {
actions: Vec<Action>,
}
impl Transaction {
pub fn install<Pkg>(src: &mut Pkg, base_dir: impl AsRef<Path>) -> Result<Self, Error>
where
Pkg: PackageSrc<Err = Error> + PackageSrcExt<File>,
{
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()?;
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));
symlink(sym_target, &tmp_path)
.map_err(wrap_io_err!(tmp_path, "Symlinking to tmp"))?;
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)?;
}
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<File>,
{
let old_entries = old.read_entries()?;
let new_entries = new.read_entries()?;
let mut actions = old_entries
.iter()
.filter(|old_e| {
!new_entries
.iter()
.any(|new_e| new_e.blake3() == old_e.blake3())
})
.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 mut candidate = File::open(&target_path).map_err(|source| Error::Io {
source,
path: Some(target_path.clone()),
context: "Opening candidate",
})?;
copy_and_hash(&mut candidate, &mut io::sink(), &mut buf).map_err(|source| {
Error::Io {
source,
path: Some(target_path.clone()),
context: "Hashing file for entry",
}
})?;
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(Error::FailedCommit {
source: Box::new(err),
changed: count,
remaining: 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(Error::FailedCommit {
source: Box::new(err),
changed: count,
remaining: self.actions.len(),
});
} else {
last_failed = true;
}
}
count += 1;
}
Ok(count)
}
}