diff options
| -rw-r--r-- | src/manifest.rs | 23 | ||||
| -rw-r--r-- | tests/compare.rs | 28 |
2 files changed, 46 insertions, 5 deletions
diff --git a/src/manifest.rs b/src/manifest.rs index 307913e..6168706 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -1,6 +1,7 @@ -use std::collections::HashSet; +use std::collections::{BTreeMap, HashSet}; use std::fs; use std::io::Read; +use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use anyhow::{bail, Context, Result}; @@ -154,8 +155,15 @@ impl ArtifactManifest { fn collect_files(rootfs: &Path) -> Result<Vec<FileRecord>> { let mut files = Vec::new(); - for entry in WalkDir::new(rootfs).follow_links(false).min_depth(1) { - let entry = entry.with_context(|| format!("walk rootfs {}", rootfs.display()))?; + let mut entries = WalkDir::new(rootfs) + .follow_links(false) + .min_depth(1) + .into_iter() + .collect::<std::result::Result<Vec<_>, _>>() + .with_context(|| format!("walk rootfs {}", rootfs.display()))?; + entries.sort_by_key(|entry| entry.path().to_path_buf()); + let mut hardlink_targets: BTreeMap<(u64, u64), String> = BTreeMap::new(); + for entry in entries { let relative = entry.path().strip_prefix(rootfs).expect("walk entry is below rootfs"); if is_internal_metadata_path(&portable_path(relative)?) { continue; @@ -163,7 +171,14 @@ fn collect_files(rootfs: &Path) -> Result<Vec<FileRecord>> { let path = portable_path(relative)?; let file_type = entry.file_type(); if file_type.is_file() { - files.push(FileRecord::file(path, sha256_file(entry.path())?)); + let metadata = entry.metadata().with_context(|| format!("inspect rootfs file {}", entry.path().display()))?; + let key = (metadata.dev(), metadata.ino()); + if let Some(target) = hardlink_targets.get(&key) { + files.push(FileRecord::hardlink(path, target.clone())); + } else { + hardlink_targets.insert(key, path.clone()); + files.push(FileRecord::file(path, sha256_file(entry.path())?)); + } } else if file_type.is_symlink() { let target = fs::read_link(entry.path()) .with_context(|| format!("read rootfs symlink {}", entry.path().display()))?; diff --git a/tests/compare.rs b/tests/compare.rs index 7dd44e2..a175195 100644 --- a/tests/compare.rs +++ b/tests/compare.rs @@ -3,7 +3,7 @@ use alt_controller_image::archive::NativeTarWriter; use alt_controller_image::manifest::{ ArchiveMemberRecord, ArtifactManifest, FileRecord, InitrdRecord, PackageRecord, ServiceRecord, }; -use std::fs; +use std::fs::{self, hard_link}; use std::io::Cursor; use std::os::unix::fs::symlink; use tempfile::tempdir; @@ -254,6 +254,32 @@ fn collects_hardlink_targets_from_an_archive_for_semantic_comparison() { } #[test] +fn rootfs_collection_preserves_hardlink_facts_like_archive_collection() { + let fixture = tempdir().expect("temporary directory"); + let rootfs = fixture.path().join("rootfs"); + fs::create_dir_all(rootfs.join("usr/bin")).expect("create binary directory"); + fs::write(rootfs.join("usr/bin/controller"), "controller\n").expect("write binary"); + hard_link( + rootfs.join("usr/bin/controller"), + rootfs.join("usr/bin/controller-link"), + ) + .expect("create hardlink"); + let artifact = fixture.path().join("controller.tar"); + NativeTarWriter::new() + .write(&rootfs, &artifact) + .expect("write native archive"); + + let rootfs_manifest = ArtifactManifest::collect(&rootfs, vec![], None, &artifact) + .expect("collect rootfs facts"); + let archive_manifest = ArtifactManifest::collect_archive(&artifact).expect("collect archive facts"); + + assert_eq!(rootfs_manifest.files, archive_manifest.files); + assert!(rootfs_manifest.files.iter().any(|record| { + record == &FileRecord::hardlink("usr/bin/controller-link", "usr/bin/controller") + })); +} + +#[test] fn normalizes_dot_prefixed_paths_from_a_legacy_tree_archive() { let fixture = tempdir().expect("temporary directory"); let rootfs = fixture.path().join("rootfs"); |