summaryrefslogtreecommitdiff
path: root/src/app/files.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/files.rs')
-rw-r--r--src/app/files.rs77
1 files changed, 77 insertions, 0 deletions
diff --git a/src/app/files.rs b/src/app/files.rs
index 2c612a9..e96e5a4 100644
--- a/src/app/files.rs
+++ b/src/app/files.rs
@@ -161,6 +161,34 @@ pub(super) fn copy_source(root: &Path, destination: &Path) -> Result<()> {
Ok(())
}
+pub(super) fn copy_runtime(root: &Path, destination: &Path) -> Result<()> {
+ let archive = root.join("bin/guix-runtime.tar.gz");
+ let channels = root.join("bin/guix-channels.scm");
+ ensure!(
+ archive.is_file() == channels.is_file(),
+ "Guix runtime archive and channel provenance must both be present"
+ );
+ if archive.is_file() {
+ let bin = destination.join("bin");
+ fs::create_dir_all(&bin)?;
+ for name in ["guix-runtime.tar.gz", "guix-channels.scm"] {
+ let source = root.join("bin").join(name);
+ regular_tree(&source)?;
+ let pending = tempfile::NamedTempFile::new_in(&bin)?;
+ fs::copy(source, pending.path())?;
+ pending.as_file().sync_all()?;
+ pending
+ .persist(bin.join(name))
+ .map_err(|error| error.error)?;
+ }
+ }
+ let launcher = root.join("infra/guix/run.sh");
+ if launcher.is_file() {
+ write(&destination.join("bin/run"), &fs::read(launcher)?, 0o755)?;
+ }
+ Ok(())
+}
+
pub(super) fn inventory(root: &Path) -> Result<BTreeMap<String, String>> {
let mut files = BTreeMap::new();
for item in WalkDir::new(root).follow_links(false) {
@@ -279,6 +307,55 @@ mod tests {
use super::{copy_source, extract, manifest, member_path, pack, verify};
#[test]
+ fn runtime_artifacts_survive_archive_round_trip() -> Result<()> {
+ let source = tempfile::tempdir()?;
+ let stage = tempfile::tempdir()?;
+ fs::create_dir_all(source.path().join("bin"))?;
+ fs::create_dir_all(source.path().join("infra/guix"))?;
+ fs::write(source.path().join("infra/guix/run.sh"), "launcher")?;
+ super::copy_runtime(source.path(), stage.path())?;
+ anyhow::ensure!(stage.path().join("bin/run").is_file(), "Launcher missing");
+ fs::write(source.path().join("bin/guix-runtime.tar.gz"), "opaque pack")?;
+ anyhow::ensure!(
+ super::copy_runtime(source.path(), stage.path()).is_err(),
+ "Missing provenance accepted"
+ );
+ fs::write(
+ source.path().join("bin/guix-channels.scm"),
+ "pinned channels",
+ )?;
+ super::copy_runtime(source.path(), stage.path())?;
+ fs::set_permissions(
+ stage.path().join("bin/guix-runtime.tar.gz"),
+ std::os::unix::fs::PermissionsExt::from_mode(0o400),
+ )?;
+ fs::write(
+ source.path().join("bin/guix-runtime.tar.gz"),
+ "updated pack",
+ )?;
+ super::copy_runtime(source.path(), stage.path())?;
+ anyhow::ensure!(
+ fs::read_to_string(stage.path().join("bin/guix-runtime.tar.gz"))? == "updated pack",
+ "Read-only runtime was not replaced"
+ );
+ manifest(stage.path(), "backup")?;
+ let output = source.path().join("backup.tar.gz");
+ pack(stage.path(), &output)?;
+ let restored = source.path().join("restored");
+ extract(&output, &restored)?;
+ verify(&restored)?;
+ anyhow::ensure!(
+ fs::read_to_string(restored.join("bin/guix-runtime.tar.gz"))? == "updated pack",
+ "Runtime changed during recovery"
+ );
+ anyhow::ensure!(
+ fs::read_to_string(restored.join("bin/guix-channels.scm"))? == "pinned channels",
+ "Provenance changed during recovery"
+ );
+ Ok(())
+ }
+
+ #[test]
fn rejects_paths_outside_archive_root() -> Result<()> {
let destination = Path::new("/restore");
for name in ["/etc/passwd", "../escape", "stack/../escape", "other/file"] {