diff options
Diffstat (limited to 'src/manifest.rs')
| -rw-r--r-- | src/manifest.rs | 202 |
1 files changed, 155 insertions, 47 deletions
diff --git a/src/manifest.rs b/src/manifest.rs index 067dc9a..0821fab 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -4,7 +4,7 @@ use std::io::{Read, Write}; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use tar::{Archive, EntryType}; @@ -34,18 +34,41 @@ impl ArtifactManifest { validate_file_records(&files)?; validate_initrd_record(initrd.as_ref())?; validate_manifest_paths(&files, initrd.as_ref(), &archive_members)?; - ensure_unique("package", packages.iter().map(|record| record.name.as_str()))?; + ensure_unique( + "package", + packages.iter().map(|record| record.name.as_str()), + )?; ensure_unique("file", files.iter().map(|record| record.path.as_str()))?; - ensure_unique("service", services.iter().map(|record| record.name.as_str()))?; - ensure_unique("archive member", archive_members.iter().map(|record| record.path.as_str()))?; - Ok(Self { packages, files, initrd, services, archive_members }) + ensure_unique( + "service", + services.iter().map(|record| record.name.as_str()), + )?; + ensure_unique( + "archive member", + archive_members.iter().map(|record| record.path.as_str()), + )?; + Ok(Self { + packages, + files, + initrd, + services, + archive_members, + }) } pub fn load(path: impl AsRef<Path>) -> Result<Self> { let path = path.as_ref(); - let text = fs::read_to_string(path).with_context(|| format!("read artifact manifest {}", path.display()))?; - let manifest: Self = toml::from_str(&text).with_context(|| format!("parse artifact manifest {}", path.display()))?; - Self::new(manifest.packages, manifest.files, manifest.initrd, manifest.services, manifest.archive_members) + let text = fs::read_to_string(path) + .with_context(|| format!("read artifact manifest {}", path.display()))?; + let manifest: Self = toml::from_str(&text) + .with_context(|| format!("parse artifact manifest {}", path.display()))?; + Self::new( + manifest.packages, + manifest.files, + manifest.initrd, + manifest.services, + manifest.archive_members, + ) } pub fn write_beside(&self, artifact: impl AsRef<Path>) -> Result<PathBuf> { @@ -67,9 +90,9 @@ impl ArtifactManifest { /// directory. pub fn path_beside(artifact: impl AsRef<Path>) -> Result<PathBuf> { let artifact = artifact.as_ref(); - let name = artifact - .file_name() - .ok_or_else(|| anyhow::anyhow!("artifact path has no filename: {}", artifact.display()))?; + let name = artifact.file_name().ok_or_else(|| { + anyhow::anyhow!("artifact path has no filename: {}", artifact.display()) + })?; let mut manifest_name = name.to_os_string(); manifest_name.push(".manifest.toml"); Ok(artifact.with_file_name(manifest_name)) @@ -134,7 +157,10 @@ impl ArtifactManifest { EntryType::Directory => "directory", EntryType::Symlink => "symlink", EntryType::Link => "hardlink", - other => bail!("unsupported archive member type {other:?} in {}", archive_path.display()), + other => bail!( + "unsupported archive member type {other:?} in {}", + archive_path.display() + ), }; archive_members.push(ArchiveMemberRecord::new(path.clone(), kind)); @@ -144,7 +170,10 @@ impl ArtifactManifest { 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()); + bail!( + "multiple initrd images found in archive {}", + archive_path.display() + ); } } if !path.starts_with("boot/initrd-") || !path.ends_with(".img") { @@ -194,7 +223,10 @@ fn collect_files(rootfs: &Path, initrd: Option<&InitrdRecord>) -> Result<Vec<Fil 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"); + let relative = entry + .path() + .strip_prefix(rootfs) + .expect("walk entry is below rootfs"); if is_internal_metadata_path(&portable_path(relative)?) { continue; } @@ -204,7 +236,9 @@ fn collect_files(rootfs: &Path, initrd: Option<&InitrdRecord>) -> Result<Vec<Fil } let file_type = entry.file_type(); if file_type.is_file() { - let metadata = entry.metadata().with_context(|| format!("inspect rootfs file {}", entry.path().display()))?; + 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())); @@ -230,7 +264,8 @@ fn collect_services(rootfs: &Path) -> Result<Vec<ServiceRecord>> { } let mut services = Vec::new(); for entry in WalkDir::new(&system).follow_links(false).min_depth(2) { - let entry = entry.with_context(|| format!("walk service state directory {}", system.display()))?; + let entry = + entry.with_context(|| format!("walk service state directory {}", system.display()))?; let parent_is_wants_directory = entry .path() .parent() @@ -239,10 +274,9 @@ fn collect_services(rootfs: &Path) -> Result<Vec<ServiceRecord>> { if !parent_is_wants_directory || !entry.file_type().is_symlink() { continue; } - let name = entry - .file_name() - .to_str() - .ok_or_else(|| anyhow::anyhow!("non-UTF-8 service name in {}", entry.path().display()))?; + let name = entry.file_name().to_str().ok_or_else(|| { + anyhow::anyhow!("non-UTF-8 service name in {}", entry.path().display()) + })?; if name.ends_with(".service") { services.push(ServiceRecord::new(name, true)); } @@ -257,11 +291,16 @@ fn coalesce_enabled_services(mut services: Vec<ServiceRecord>) -> Vec<ServiceRec } fn collect_archive_members(archive_path: &Path) -> Result<Vec<ArchiveMemberRecord>> { - let file = fs::File::open(archive_path).with_context(|| format!("open archive {}", archive_path.display()))?; + let file = fs::File::open(archive_path) + .with_context(|| format!("open archive {}", archive_path.display()))?; let mut archive = Archive::new(file); let mut members = Vec::new(); - for entry in archive.entries().with_context(|| format!("read archive {}", archive_path.display()))? { - let entry = entry.with_context(|| format!("read archive member from {}", archive_path.display()))?; + for entry in archive + .entries() + .with_context(|| format!("read archive {}", archive_path.display()))? + { + let entry = entry + .with_context(|| format!("read archive member from {}", archive_path.display()))?; let path = portable_path(&entry.path()?)?; if is_archive_root_path(&path) || is_internal_metadata_path(&path) { continue; @@ -271,7 +310,10 @@ fn collect_archive_members(archive_path: &Path) -> Result<Vec<ArchiveMemberRecor EntryType::Directory => "directory", EntryType::Symlink => "symlink", EntryType::Link => "hardlink", - other => bail!("unsupported archive member type {other:?} in {}", archive_path.display()), + other => bail!( + "unsupported archive member type {other:?} in {}", + archive_path.display() + ), }; members.push(ArchiveMemberRecord::new(path, kind)); } @@ -279,7 +321,8 @@ 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()))?; + let mut file = + fs::File::open(path).with_context(|| format!("open rootfs file {}", path.display()))?; sha256_reader(&mut file, path) } @@ -287,7 +330,9 @@ fn sha256_reader(reader: &mut impl Read, source: &Path) -> Result<String> { let mut hasher = Sha256::new(); let mut buffer = [0; 8192]; loop { - let read = reader.read(&mut buffer).with_context(|| format!("read {}", source.display()))?; + let read = reader + .read(&mut buffer) + .with_context(|| format!("read {}", source.display()))?; if read == 0 { break; } @@ -298,9 +343,17 @@ fn sha256_reader(reader: &mut impl Read, source: &Path) -> Result<String> { fn portable_path(path: &Path) -> Result<String> { if path.is_absolute() { - bail!("absolute path is not valid in an artifact manifest: {}", path.display()); + bail!( + "absolute path is not valid in an artifact manifest: {}", + path.display() + ); } - let value = path.to_str().ok_or_else(|| anyhow::anyhow!("non-UTF-8 path is not valid in an artifact manifest: {}", path.display()))?; + let value = path.to_str().ok_or_else(|| { + anyhow::anyhow!( + "non-UTF-8 path is not valid in an artifact manifest: {}", + path.display() + ) + })?; if value.is_empty() { bail!("empty path is not valid in an artifact manifest"); } @@ -310,19 +363,28 @@ fn portable_path(path: &Path) -> Result<String> { std::path::Component::Normal(part) => normalized.push(part), std::path::Component::CurDir => {} std::path::Component::ParentDir => { - bail!("parent path is not valid in an artifact manifest: {}", path.display()); + bail!( + "parent path is not valid in an artifact manifest: {}", + path.display() + ); } std::path::Component::RootDir | std::path::Component::Prefix(_) => { - bail!("absolute path is not valid in an artifact manifest: {}", path.display()); + bail!( + "absolute path is not valid in an artifact manifest: {}", + path.display() + ); } } } if normalized.as_os_str().is_empty() { return Ok(".".into()); } - let normalized = normalized - .to_str() - .ok_or_else(|| anyhow::anyhow!("non-UTF-8 path is not valid in an artifact manifest: {}", path.display()))?; + let normalized = normalized.to_str().ok_or_else(|| { + anyhow::anyhow!( + "non-UTF-8 path is not valid in an artifact manifest: {}", + path.display() + ) + })?; Ok(normalized.to_owned()) } @@ -349,7 +411,8 @@ fn is_enabled_service(path: &str) -> bool { } fn is_internal_metadata_path(path: &str) -> bool { - path.split('/').any(|component| matches!(component, ".host" | ".fakedata")) + path.split('/') + .any(|component| matches!(component, ".host" | ".fakedata")) } fn is_archive_root_path(path: &str) -> bool { @@ -364,7 +427,12 @@ fn service_name(path: &str) -> Result<&str> { } 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()))?; + 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() { bail!("empty symlink target is not valid in an artifact manifest"); } @@ -376,7 +444,11 @@ fn validate_manifest_paths( initrd: Option<&InitrdRecord>, archive_members: &[ArchiveMemberRecord], ) -> Result<()> { - for path in files.iter().map(|record| record.path.as_str()).chain(archive_members.iter().map(|record| record.path.as_str())) { + for path in files + .iter() + .map(|record| record.path.as_str()) + .chain(archive_members.iter().map(|record| record.path.as_str())) + { portable_path(Path::new(path))?; } if let Some(initrd) = initrd { @@ -386,10 +458,11 @@ fn validate_manifest_paths( } fn validate_initrd_record(record: Option<&InitrdRecord>) -> Result<()> { - if let Some(record) = record { - if record.sha256.is_empty() { + match record { + Some(record) if record.sha256.is_empty() => { bail!("initrd record requires a sha256 digest: {}", record.path); } + _ => {} } Ok(()) } @@ -407,13 +480,21 @@ fn validate_file_records(records: &[FileRecord]) -> Result<()> { } "symlink" | "hardlink" => { if record.digest.is_some() { - bail!("{} record cannot carry a digest: {}", record.kind, record.path); + bail!( + "{} record cannot carry a digest: {}", + record.kind, + record.path + ); } if record.target.as_deref().is_none_or(str::is_empty) { bail!("{} record requires a target: {}", record.kind, record.path); } } - _ => bail!("unsupported file record kind {}: {}", record.kind, record.path), + _ => bail!( + "unsupported file record kind {}: {}", + record.kind, + record.path + ), } } Ok(()) @@ -437,7 +518,10 @@ pub struct PackageRecord { impl PackageRecord { pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self { - Self { name: name.into(), version: version.into() } + Self { + name: name.into(), + version: version.into(), + } } } @@ -451,15 +535,30 @@ pub struct FileRecord { impl FileRecord { pub fn file(path: impl Into<String>, digest: impl Into<String>) -> Self { - Self { path: path.into(), kind: "file".into(), digest: Some(digest.into()), target: None } + Self { + path: path.into(), + kind: "file".into(), + digest: Some(digest.into()), + target: None, + } } pub fn symlink(path: impl Into<String>, target: impl Into<String>) -> Self { - Self { path: path.into(), kind: "symlink".into(), digest: None, target: Some(target.into()) } + Self { + path: path.into(), + kind: "symlink".into(), + digest: None, + target: Some(target.into()), + } } pub fn hardlink(path: impl Into<String>, target: impl Into<String>) -> Self { - Self { path: path.into(), kind: "hardlink".into(), digest: None, target: Some(target.into()) } + Self { + path: path.into(), + kind: "hardlink".into(), + digest: None, + target: Some(target.into()), + } } } @@ -471,7 +570,10 @@ pub struct InitrdRecord { impl InitrdRecord { pub fn new(path: impl Into<String>, sha256: impl Into<String>) -> Self { - Self { path: path.into(), sha256: sha256.into() } + Self { + path: path.into(), + sha256: sha256.into(), + } } } @@ -483,7 +585,10 @@ pub struct ServiceRecord { impl ServiceRecord { pub fn new(name: impl Into<String>, enabled: bool) -> Self { - Self { name: name.into(), enabled } + Self { + name: name.into(), + enabled, + } } } @@ -495,6 +600,9 @@ pub struct ArchiveMemberRecord { impl ArchiveMemberRecord { pub fn new(path: impl Into<String>, kind: impl Into<String>) -> Self { - Self { path: path.into(), kind: kind.into() } + Self { + path: path.into(), + kind: kind.into(), + } } } |