From 1151de44e1950faadb76496cfa52a3c658fcdf4a Mon Sep 17 00:00:00 2001
From: Wesley Hershberger <mggmugginsmc@gmail.com>
Date: Wed, 26 Aug 2020 11:42:22 -0400
Subject: [PATCH] Refactor transactions

Instead of creating one transaction for all operations and calling a
method on that transaction to add installs or removes, the operation
methods are constructors for transactions, and a transaction represents
the temprorary state on the filesystem of an operation on an individual
package or pair of packages.

Probably some convenience methods should be implemented for
Vec<Transaction> for easy comitting, but the idea is to expose the
stateful nature of the transactions to library users so that they can do
all the expensive parts of a large operation and commit all package
changes at once.
---
 pkgar/src/bin.rs           | 10 +++----
 pkgar/src/transaction.rs   | 61 ++++++++++++++++++++++----------------
 pkgar/tests/transaction.rs |  9 ++----
 3 files changed, 43 insertions(+), 37 deletions(-)

diff --git a/pkgar/src/bin.rs b/pkgar/src/bin.rs
index 2296350..2c2f2d5 100644
--- a/pkgar/src/bin.rs
+++ b/pkgar/src/bin.rs
@@ -226,9 +226,8 @@ pub fn extract(
 
     let mut package = PackageFile::new(archive_path, &pkey)?;
 
-    let mut transaction = Transaction::new(base_dir);
-    transaction.install(&mut package)?;
-    transaction.commit()?;
+    Transaction::install(&mut package, base_dir)?
+        .commit()?;
 
     Ok(())
 }
@@ -242,9 +241,8 @@ pub fn remove(
 
     let mut package = PackageFile::new(archive_path, &pkey)?;
 
-    let mut transaction = Transaction::new(base_dir);
-    transaction.remove(&mut package)?;
-    transaction.commit()?;
+    Transaction::remove(&mut package, base_dir)?
+        .commit()?;
 
     Ok(())
 }
diff --git a/pkgar/src/transaction.rs b/pkgar/src/transaction.rs
index b3b8778..620ab02 100644
--- a/pkgar/src/transaction.rs
+++ b/pkgar/src/transaction.rs
@@ -52,32 +52,28 @@ impl Action {
 
 pub struct Transaction {
     actions: Vec<Action>,
-    basedir: PathBuf,
 }
 
 impl Transaction {
-    pub fn new(basedir: impl AsRef<Path>) -> Transaction {
-        Transaction {
-            actions: Vec::new(),
-            basedir: basedir.as_ref().to_path_buf(),
-        }
-    }
-    
-    pub fn install<Pkg>(&mut self, src: &mut Pkg) -> Result<(), Error>
+    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()?;
-        self.actions.reserve(entries.len());
+        let mut actions = Vec::with_capacity(entries.len());
         
         for entry in entries {
             let relative_path = entry.check_path()
                 .map_err(|e| e.path(src.path()) )?;
             
-            let target_path = self.basedir.join(relative_path);
+            let target_path = base_dir.as_ref().join(relative_path);
             //HELP: Under what circumstances could this ever fail?
-            assert!(target_path.starts_with(&self.basedir), "target path was not in the base 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())?;
             
@@ -131,61 +127,76 @@ impl Transaction {
             entry.verify(entry_data_hash, entry_data_size)
                 .map_err(|e| e.path(src.path()))?;
             
-            self.actions.push(Action::Rename(tmp_path, target_path))
+            actions.push(Action::Rename(tmp_path, target_path))
         }
-        Ok(())
+        Ok(Transaction {
+            actions,
+        })
     }
     
-    pub fn replace<Pkg>(&mut self, old: &mut Pkg, new: &mut Pkg) -> Result<(), Error>
+    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()?;
         
         // All the files that are present in old but not in new
-        let mut removes = old_entries.iter()
+        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 = self.basedir
+                let target_path = base_dir.as_ref()
                     .join(e.check_path()?);
                 Ok(Action::Remove(target_path))
             })
             .collect::<Result<Vec<Action>, Error>>()?;
-        self.actions.append(&mut removes);
         
         //TODO: Don't force a re-read of all the entries for the new package
-        self.install(new)
+        let mut trans = Transaction::install(new, base_dir)?;
+        trans.actions.append(&mut actions);
+        Ok(trans)
     }
     
-    pub fn remove<Pkg>(&mut self, pkg: &mut Pkg) -> Result<(), Error>
+    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()?;
-        self.actions.reserve(entries.len());
+        let mut actions = Vec::with_capacity(entries.len());
         
         for entry in entries {
             let relative_path = entry.check_path()?;
             
-            let target_path = self.basedir.join(relative_path);
+            let target_path = base_dir.as_ref()
+                .join(relative_path);
             // Under what circumstances could this ever fail?
-            assert!(target_path.starts_with(&self.basedir), "target path was not in the base path");
+            assert!(target_path.starts_with(&base_dir),
+                "target path was not in the base path");
             
             let candidate = File::open(&target_path)
                 .map_err(|e| Error::from(e).path(&target_path) )?;
             
+            // Ensure that the deletion candidate on disk has not been modified
             copy_and_hash(candidate, io::sink(), &mut buf)
                 .map_err(|e| Error::from(e)
                     .reason(format!("Hashing file for entry: '{}'", relative_path.display()))
                     .path(&target_path)
                 )?;
             
-            self.actions.push(Action::Remove(target_path));
+            actions.push(Action::Remove(target_path));
         }
-        Ok(())
+        Ok(Transaction {
+            actions
+        })
     }
     
     pub fn commit(&mut self) -> Result<usize, Error> {
diff --git a/pkgar/tests/transaction.rs b/pkgar/tests/transaction.rs
index d1bebaf..474edbe 100644
--- a/pkgar/tests/transaction.rs
+++ b/pkgar/tests/transaction.rs
@@ -54,8 +54,7 @@ fn build_install_update_remove() -> Result<(), Box<dyn Error>> {
     let mut src_pkg = PackageFile::new(tmp.file("pkgar-src-1.pkgar"), &pkey_file.pkey)?;
     
     println!("Install archive");
-    let mut install = Transaction::new(tmp.dir("installroot"));
-    install.install(&mut src_pkg)?;
+    let mut install = Transaction::install(&mut src_pkg, tmp.dir("installroot"))?;
     install.commit()?;
     
     println!("Modify build");
@@ -70,13 +69,11 @@ fn build_install_update_remove() -> Result<(), Box<dyn Error>> {
     let mut src2_pkg = PackageFile::new(tmp.file("pkgar-src-2.pkgar"), &pkey_file.pkey)?;
     
     println!("Upgrade archive");
-    let mut update = Transaction::new(tmp.dir("installroot"));
-    update.replace(&mut src_pkg, &mut src2_pkg)?;
+    let mut update = Transaction::replace(&mut src_pkg, &mut src2_pkg, tmp.dir("installroot"))?;
     update.commit()?;
     
     println!("Uninstall archive");
-    let mut remove = Transaction::new(tmp.dir("installroot"));
-    remove.remove(&mut src2_pkg)?;
+    let mut remove = Transaction::remove(&mut src2_pkg, tmp.dir("installroot"))?;
     remove.commit()?;
     
     assert_eq!(fs::read_dir(tmp.dir("installroot"))?.count(), 0);
-- 
GitLab