summaryrefslogtreecommitdiff
path: root/src/manifest.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/manifest.rs')
-rw-r--r--src/manifest.rs87
1 files changed, 86 insertions, 1 deletions
diff --git a/src/manifest.rs b/src/manifest.rs
index 98dc42d..57e57c5 100644
--- a/src/manifest.rs
+++ b/src/manifest.rs
@@ -74,6 +74,67 @@ impl ArtifactManifest {
collect_archive_members(archive.as_ref())?,
)
}
+
+ /// Collect portable semantic facts from an already packaged rootfs archive.
+ /// This permits comparison with legacy artifacts when their build root is
+ /// unavailable. RPM package metadata remains unavailable in a plain tar
+ /// archive and is consequently represented by an empty package set.
+ pub fn collect_archive(archive_path: impl AsRef<Path>) -> Result<Self> {
+ let archive_path = archive_path.as_ref();
+ let file = fs::File::open(archive_path)
+ .with_context(|| format!("open archive {}", archive_path.display()))?;
+ let mut archive = Archive::new(file);
+ let mut files = Vec::new();
+ let mut services = Vec::new();
+ let mut archive_members = Vec::new();
+ let mut initrd = None;
+
+ for entry in archive
+ .entries()
+ .with_context(|| format!("read archive {}", archive_path.display()))?
+ {
+ let mut entry = entry
+ .with_context(|| format!("read archive member from {}", archive_path.display()))?;
+ let path = portable_path(&entry.path()?)?;
+ let entry_type = entry.header().entry_type();
+ let kind = match entry_type {
+ EntryType::Regular => "file",
+ EntryType::Directory => "directory",
+ EntryType::Symlink => "symlink",
+ EntryType::Link => "hardlink",
+ other => bail!("unsupported archive member type {other:?} in {}", archive_path.display()),
+ };
+ archive_members.push(ArchiveMemberRecord::new(path.clone(), kind));
+
+ match entry_type {
+ EntryType::Regular => {
+ let digest = sha256_reader(&mut entry, archive_path)?;
+ if path.starts_with("boot/initrd-") && path.ends_with(".img") {
+ let record = InitrdRecord::new(path.clone(), digest.clone());
+ if initrd.replace(record).is_some() {
+ bail!("multiple initrd images found in archive {}", archive_path.display());
+ }
+ }
+ files.push(FileRecord::file(path, digest));
+ }
+ EntryType::Symlink => {
+ let target = entry
+ .link_name()
+ .with_context(|| format!("read archive symlink target for {path}"))?
+ .ok_or_else(|| anyhow::anyhow!("archive symlink has no target: {path}"))?;
+ files.push(FileRecord::symlink(path.clone(), link_target(&target)?));
+ if is_enabled_service(&path) {
+ services.push(ServiceRecord::new(service_name(&path)?, true));
+ }
+ }
+ EntryType::Directory => {}
+ EntryType::Link => {}
+ _ => unreachable!("entry type was checked above"),
+ }
+ }
+
+ Self::new(Vec::new(), files, initrd, services, archive_members)
+ }
}
fn collect_files(rootfs: &Path) -> Result<Vec<FileRecord>> {
@@ -125,6 +186,7 @@ fn collect_archive_members(archive_path: &Path) -> Result<Vec<ArchiveMemberRecor
EntryType::Regular => "file",
EntryType::Directory => "directory",
EntryType::Symlink => "symlink",
+ EntryType::Link => "hardlink",
other => bail!("unsupported archive member type {other:?} in {}", archive_path.display()),
};
members.push(ArchiveMemberRecord::new(portable_path(&entry.path()?)?, kind));
@@ -134,10 +196,14 @@ fn collect_archive_members(archive_path: &Path) -> Result<Vec<ArchiveMemberRecor
fn sha256_file(path: &Path) -> Result<String> {
let mut file = fs::File::open(path).with_context(|| format!("open rootfs file {}", path.display()))?;
+ sha256_reader(&mut file, path)
+}
+
+fn sha256_reader(reader: &mut impl Read, source: &Path) -> Result<String> {
let mut hasher = Sha256::new();
let mut buffer = [0; 8192];
loop {
- let read = file.read(&mut buffer).with_context(|| format!("read rootfs file {}", path.display()))?;
+ let read = reader.read(&mut buffer).with_context(|| format!("read {}", source.display()))?;
if read == 0 {
break;
}
@@ -154,9 +220,28 @@ fn portable_path(path: &Path) -> Result<String> {
if value.is_empty() || value == "." {
bail!("empty path is not valid in an artifact manifest");
}
+ if path
+ .components()
+ .any(|component| matches!(component, std::path::Component::ParentDir))
+ {
+ bail!("parent path is not valid in an artifact manifest: {}", path.display());
+ }
Ok(value.to_owned())
}
+fn is_enabled_service(path: &str) -> bool {
+ path.starts_with("etc/systemd/system/")
+ && path.contains(".target.wants/")
+ && path.ends_with(".service")
+}
+
+fn service_name(path: &str) -> Result<&str> {
+ path.rsplit('/')
+ .next()
+ .filter(|name| !name.is_empty())
+ .ok_or_else(|| anyhow::anyhow!("invalid service path in archive: {path}"))
+}
+
fn link_target(path: &Path) -> Result<String> {
let value = path.to_str().ok_or_else(|| anyhow::anyhow!("non-UTF-8 symlink target is not valid in an artifact manifest: {}", path.display()))?;
if value.is_empty() {