diff --git a/crates/composefs-ctl/src/lib.rs b/crates/composefs-ctl/src/lib.rs index d6783da2..e015621a 100644 --- a/crates/composefs-ctl/src/lib.rs +++ b/crates/composefs-ctl/src/lib.rs @@ -582,6 +582,21 @@ enum OstreeCommand { #[arg(add = ArgValueCompleter::new(complete_ostree_refs))] name: String, }, + /// Create an ostree commit from a composefs image in the repository + /// + /// The image is specified by its object ID or refs/ name (the same + /// format used by `cfsctl mount` and `cfsctl image-objects`). + Commit { + /// Composefs image ID or refs/ name + #[arg(add = ArgValueCompleter::new(complete_image_refs))] + image: String, + /// Ostree ref name to tag the commit with + #[clap(long)] + reference: Option, + /// One-line commit subject + #[clap(long, default_value = "")] + subject: String, + }, /// List all ostree commits in the repository #[clap(name = "images")] ListCommits, @@ -1834,6 +1849,46 @@ where OstreeCommand::Untag { ref name } => { composefs_ostree::untag(&repo, name)?; } + OstreeCommand::Commit { + ref image, + ref reference, + ref subject, + } => { + use std::time::{SystemTime, UNIX_EPOCH}; + + let (img_fd, _) = repo.open_image(image)?; + let mut img_buf = Vec::new(); + std::fs::File::from(img_fd).read_to_end(&mut img_buf)?; + let fs = composefs::erofs::reader::erofs_to_filesystem(&img_buf)?; + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let mut commit_meta = composefs_ostree::ostree::CommitMetadata::default() + .subject(subject.as_str()) + .timestamp(timestamp); + if let Some(ref_name) = reference { + commit_meta = commit_meta.add_metadata( + "ostree.ref-binding", + composefs_ostree::ostree::MetadataValue::StringArray(vec![ + ref_name.clone(), + ]), + ); + } + + let (verity, commit_id) = composefs_ostree::commit_filesystem( + &repo, + &fs, + commit_meta, + reference.as_deref(), + )?; + println!("commit {commit_id}"); + println!("verity {}", verity.to_hex()); + if let Some(ref_name) = reference { + println!("tagged {ref_name}"); + } + } OstreeCommand::ListCommits => { let commits = composefs_ostree::list_commits(&repo)?; if commits.is_empty() { diff --git a/crates/composefs-integration-tests/src/tests/ostree.rs b/crates/composefs-integration-tests/src/tests/ostree.rs index 6f5a7ec3..6acb71ef 100644 --- a/crates/composefs-integration-tests/src/tests/ostree.rs +++ b/crates/composefs-integration-tests/src/tests/ostree.rs @@ -512,6 +512,37 @@ fn test_ostree_pull_remote_diff_delta() -> Result<()> { } integration_test!(test_ostree_pull_remote_diff_delta); +fn test_ostree_commit_roundtrip() -> Result<()> { + let sh = Shell::new()?; + let tmpdir = TempDir::new()?; + let content = create_ostree_test_content(tmpdir.path(), 1)?; + + let ostree_repo_path = tmpdir.path().join("ostree-repo"); + init_ostree_repo(&sh, &ostree_repo_path, "archive-z2")?; + let original_commit_id = commit_to_ostree(&sh, &ostree_repo_path, "original", &content)?; + + let composefs_dir = TempDir::new()?; + let repo = create_test_repository(&composefs_dir)?; + pull_and_get_image_id(&repo, &ostree_repo_path, "original", None)?; + + // Read the original commit's metadata and filesystem + let fs = composefs_ostree::create_filesystem(&repo, "original")?; + let commit = composefs_ostree::read_commit(&repo, "original")?; + let commit_meta = commit.commit_metadata(); + + // Recreate the commit — should produce the same commit ID + let (_, roundtrip_commit_id) = + composefs_ostree::commit_filesystem(&repo, &fs, commit_meta, None)?; + + assert_eq!( + original_commit_id, roundtrip_commit_id, + "roundtrip commit ID differs from original" + ); + + Ok(()) +} +integration_test!(test_ostree_commit_roundtrip); + fn test_ostree_apply_delta_offline() -> Result<()> { let sh = Shell::new()?; let tmpdir = TempDir::new()?; diff --git a/crates/composefs-ostree/src/commit.rs b/crates/composefs-ostree/src/commit.rs index 24cc08e9..4091d964 100644 --- a/crates/composefs-ostree/src/commit.rs +++ b/crates/composefs-ostree/src/commit.rs @@ -25,13 +25,32 @@ use composefs::{ }; use crate::ostree::{ - OstreeCommit, OstreeDirMeta, OstreeDirTree, OstreeFileHeader, split_sized_variant, + CommitMetadata, OstreeCommit, OstreeDirMeta, OstreeDirTree, OstreeFileHeader, + should_inline_file, split_sized_variant, }; const OSTREE_COMMIT_CONTENT_TYPE: u64 = 0xAFE138C18C463EF1; -const S_IFMT: u32 = 0o170000; -const S_IFLNK: u32 = 0o120000; +fn stat_xattrs_to_vec(stat: &Stat) -> Vec<(Vec, Vec)> { + stat.xattrs + .iter() + .map(|(k, v)| { + let mut val = v.to_vec(); + // The kernel stores security.selinux with a NUL terminator, + // but programmatic labeling (e.g. bootc's selabel()) may omit + // it. Canonicalize to always include the NUL so that ostree + // checksums match what ostree fsck expects. + if k.as_bytes() == b"security.selinux" && !val.ends_with(&[0]) { + val.push(0); + } + (k.as_bytes().to_vec(), val) + }) + .collect() +} + +const S_IFMT: u32 = composefs::erofs::format::S_IFMT as u32; +const S_IFDIR: u32 = composefs::erofs::format::S_IFDIR as u32; +const S_IFLNK: u32 = composefs::erofs::format::S_IFLNK as u32; fn xattrs_to_btreemap(xattrs: &[(Vec, Vec)]) -> BTreeMap, Box<[u8]>> { xattrs @@ -173,6 +192,170 @@ impl CommitWriter { Ok(writer) } + /// Build a commit from an in-memory filesystem tree. + /// + /// Walks the tree bottom-up, producing ostree dirtree/dirmeta/file + /// objects with correct checksums, then creates the commit object. + /// Returns the writer and the commit's ostree checksum. + pub fn from_filesystem( + repo: &Arc>, + fs: &FileSystem, + metadata: CommitMetadata, + ) -> Result<(Self, Sha256Digest)> { + let mut writer = CommitWriter::new(); + let mut leaf_cache: HashMap = HashMap::new(); + + let (root_tree_id, root_meta_id) = + Self::write_dir(repo, &mut writer, fs, &fs.root, &mut leaf_cache)?; + + let commit = metadata.into_commit(root_tree_id, root_meta_id); + let commit_data = commit.serialize(); + let commit_id: Sha256Digest = Sha256::digest(&commit_data).into(); + writer.insert(&commit_id, None, &commit_data); + writer.set_commit_id(&commit_id); + + Ok((writer, commit_id)) + } + + fn write_dir( + repo: &Arc>, + writer: &mut CommitWriter, + fs: &FileSystem, + dir: &Directory, + leaf_cache: &mut HashMap, + ) -> Result<(Sha256Digest, Sha256Digest)> { + let mut file_entries = Vec::new(); + let mut dir_entries = Vec::new(); + + for (name, inode) in dir.entries() { + let name_str = name + .to_str() + .ok_or_else(|| anyhow!("non-UTF8 filename: {:?}", name))?; + match inode { + Inode::Leaf(leaf_id, _) => { + let checksum = if let Some(cached) = leaf_cache.get(leaf_id) { + *cached + } else { + let checksum = Self::write_leaf(repo, writer, fs, *leaf_id)?; + leaf_cache.insert(*leaf_id, checksum); + checksum + }; + file_entries.push((name_str.to_string(), checksum)); + } + Inode::Directory(subdir) => { + let (tree_id, meta_id) = Self::write_dir(repo, writer, fs, subdir, leaf_cache)?; + dir_entries.push((name_str.to_string(), tree_id, meta_id)); + } + } + } + + let dirmeta = OstreeDirMeta { + uid: dir.stat.st_uid, + gid: dir.stat.st_gid, + mode: (dir.stat.st_mode & !S_IFMT) | S_IFDIR, + xattrs: stat_xattrs_to_vec(&dir.stat), + }; + let dirmeta_data = dirmeta.serialize(); + let dirmeta_id: Sha256Digest = Sha256::digest(&dirmeta_data).into(); + writer.insert(&dirmeta_id, None, &dirmeta_data); + + let dirtree = OstreeDirTree { + files: file_entries, + dirs: dir_entries, + }; + let dirtree_data = dirtree.serialize(); + let dirtree_id: Sha256Digest = Sha256::digest(&dirtree_data).into(); + writer.insert(&dirtree_id, None, &dirtree_data); + + Ok((dirtree_id, dirmeta_id)) + } + + fn write_leaf( + repo: &Arc>, + writer: &mut CommitWriter, + fs: &FileSystem, + leaf_id: LeafId, + ) -> Result { + let leaf = fs.leaf(leaf_id); + + let symlink_target = match &leaf.content { + LeafContent::Symlink(target) => target + .to_str() + .ok_or_else(|| anyhow!("non-UTF8 symlink target: {:?}", target))? + .to_string(), + _ => String::new(), + }; + + let file_size = match &leaf.content { + LeafContent::Regular(reg) => reg.file_size(), + _ => 0, + }; + + let header = OstreeFileHeader { + size: file_size, + uid: leaf.stat.st_uid, + gid: leaf.stat.st_gid, + mode: (leaf.stat.st_mode & !S_IFMT) | leaf.content.file_type_bits(), + symlink_target, + xattrs: stat_xattrs_to_vec(&leaf.stat), + }; + + let regular_header = header.serialize_regular_sized(); + let zlib_header = header.serialize_zlib_sized(); + + match &leaf.content { + LeafContent::Regular( + RegularFile::External(obj_id, size) | RegularFile::ExternalNoVerity(obj_id, size), + ) if !should_inline_file::(*size as usize) => { + // Stream through the hasher without buffering the entire file. + let mut hasher = Sha256::new(); + hasher.update(&*regular_header); + let fd = repo.open_object(obj_id)?; + let mut file = std::io::BufReader::new(std::fs::File::from(fd)); + let mut buf = [0u8; 8192]; + loop { + let n = std::io::Read::read(&mut file, &mut buf)?; + if n == 0 { + break; + } + hasher.update(&buf[..n]); + } + let checksum: Sha256Digest = hasher.finalize().into(); + writer.insert(&checksum, Some(obj_id), &zlib_header); + Ok(checksum) + } + _ => { + // Inline content: symlinks, small files, devices, etc. + let content: Option> = match &leaf.content { + LeafContent::Regular(RegularFile::Inline(data)) => Some(data.to_vec()), + LeafContent::Regular( + RegularFile::External(obj_id, _) | RegularFile::ExternalNoVerity(obj_id, _), + ) => Some(repo.read_object(obj_id)?), + LeafContent::Regular(RegularFile::Sparse(size)) => { + Some(vec![0u8; *size as usize]) + } + _ => None, + }; + + let mut hasher = Sha256::new(); + hasher.update(&*regular_header); + if let Some(ref bytes) = content { + hasher.update(bytes); + } + let checksum: Sha256Digest = hasher.finalize().into(); + + if let Some(bytes) = content { + let mut data = zlib_header; + data.with_vec(|v| v.extend_from_slice(&bytes)); + writer.insert(&checksum, None, &data); + } else { + writer.insert(&checksum, None, &zlib_header); + } + Ok(checksum) + } + } + } + pub fn serialize( &self, repo: &Arc>, diff --git a/crates/composefs-ostree/src/lib.rs b/crates/composefs-ostree/src/lib.rs index 2ac0de96..4df1a70f 100644 --- a/crates/composefs-ostree/src/lib.rs +++ b/crates/composefs-ostree/src/lib.rs @@ -346,6 +346,48 @@ pub fn create_filesystem( Ok(fs) } +/// Create an ostree commit from an in-memory filesystem tree. +/// +/// Walks the tree to produce ostree objects (dirtrees, dirmetas, file +/// headers), serializes them into a commit splitstream, and generates +/// the EROFS image. Returns the splitstream verity and the hex-encoded +/// commit ID. +/// +/// Use [`ostree::CommitMetadata::default()`] for a minimal commit, the +/// builder methods to set fields, or [`ostree::OstreeCommit::commit_metadata()`] +/// to round-trip an existing commit. +pub fn commit_filesystem( + repo: &Arc>, + fs: &FileSystem, + metadata: ostree::CommitMetadata, + reference: Option<&str>, +) -> Result<(ObjectID, String)> { + let (writer, commit_id) = CommitWriter::from_filesystem(repo, fs, metadata)?; + + let content_id = format!("ostree-commit-{}", hex::encode(commit_id)); + let ref_path = reference.map(ostree_ref_path); + + let image_id = fs.commit_image(repo, None)?; + let verity = writer.serialize(repo, &content_id, ref_path.as_deref(), Some(&image_id))?; + + Ok((verity, hex::encode(commit_id))) +} + +/// Reads and parses an ostree commit object. +/// +/// `source` can be an ostree ref name or a commit ID / prefix. +pub fn read_commit( + repo: &Repository, + source: &str, +) -> Result { + let content_id = resolve_source(repo, source)?; + let reader = CommitReader::::load(repo, &content_id)?; + let commit_data = reader + .lookup_data(&reader.commit_id())? + .ok_or_else(|| anyhow::anyhow!("commit object not found in stream"))?; + ostree::OstreeCommit::from_data(commit_data) +} + /// Prints the contents of an ostree commit object. /// /// `source` can be an ostree ref name or a commit ID / prefix. diff --git a/crates/composefs-ostree/src/ostree.rs b/crates/composefs-ostree/src/ostree.rs index 665aaea5..ee62b736 100644 --- a/crates/composefs-ostree/src/ostree.rs +++ b/crates/composefs-ostree/src/ostree.rs @@ -1,11 +1,14 @@ -//! Core ostree on-disk format types and gvariant deserialization. +//! Core ostree on-disk format types and gvariant serialization/deserialization. //! //! Defines the Rust representations of ostree objects (commits, directory //! trees, directory metadata, file headers) and their gvariant wire formats. +use std::fmt; +use std::io::Write; + use anyhow::{Result, anyhow, bail}; use gvariant::aligned_bytes::{A8, AlignedBuf, AlignedSlice, AsAligned, TryAsAligned}; -use gvariant::{Marker, Structure, gv}; +use gvariant::{Marker, Owned, SerializeTo, Structure, Variant, VariantWrap, gv}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use composefs::{fsverity::FsVerityHashValue, util::Sha256Digest}; @@ -280,6 +283,21 @@ pub struct OstreeDirMeta { } impl OstreeDirMeta { + /// Serialize to raw gvariant data. + pub fn serialize(&self) -> Vec { + let xattrs = self + .xattrs + .iter() + .map(|(k, v)| (k.as_slice(), v.as_slice())) + .collect::>(); + gv!("(uuua(ayay))").serialize_to_vec(&( + u32::to_be(self.uid), + u32::to_be(self.gid), + u32::to_be(self.mode), + &xattrs, + )) + } + /// Deserialize from raw gvariant data. pub fn from_data(data: &AlignedSlice) -> Result { let gv = gv!("(uuua(ayay))").cast(data.as_aligned()); @@ -321,6 +339,26 @@ pub struct OstreeDirTree { } impl OstreeDirTree { + /// Serialize to raw gvariant data. + /// + /// Files and directories are sorted by name, matching ostree's + /// `create_tree_variant_from_hashes` which sorts with `strcmp`. + pub fn serialize(&self) -> Vec { + let mut files: Vec<(&str, [u8; 32])> = self + .files + .iter() + .map(|(name, checksum)| (name.as_str(), *checksum)) + .collect(); + files.sort_by_key(|(name, _)| *name); + let mut dirs: Vec<(&str, [u8; 32], [u8; 32])> = self + .dirs + .iter() + .map(|(name, tree, meta)| (name.as_str(), *tree, *meta)) + .collect(); + dirs.sort_by_key(|(name, _, _)| *name); + gv!("(a(say)a(sayay))").serialize_to_vec(&(&files, &dirs)) + } + /// Deserialize from raw gvariant data. pub fn from_data(data: &AlignedSlice) -> Result { let gv = gv!("(a(say)a(sayay))").cast(data.as_aligned()); @@ -350,13 +388,82 @@ impl OstreeDirTree { } } +/// Caller-controlled fields for creating a new ostree commit. +/// +/// Use [`Default::default()`] for a minimal commit (timestamp defaults +/// to 0 — set it with [`.timestamp()`](Self::timestamp)), or +/// [`OstreeCommit::commit_metadata()`] to extract from an existing +/// commit for round-tripping. +#[derive(Debug, Default)] +pub struct CommitMetadata { + /// Checksum of the parent commit, if any. + pub parent_commit: Option, + /// Commit metadata key-value pairs. + pub metadata: Vec<(String, MetadataValue)>, + /// One-line commit subject. + pub subject: String, + /// Extended commit description. + pub body: String, + /// Commit timestamp (seconds since Unix epoch). + /// Defaults to 0; the caller should set this (e.g. to the current time). + pub timestamp: u64, +} + +impl CommitMetadata { + /// Set the commit subject. + pub fn subject(mut self, s: impl Into) -> Self { + self.subject = s.into(); + self + } + + /// Set the commit body. + pub fn body(mut self, b: impl Into) -> Self { + self.body = b.into(); + self + } + + /// Set the parent commit. + pub fn parent(mut self, p: Sha256Digest) -> Self { + self.parent_commit = Some(p); + self + } + + /// Set the commit timestamp. + pub fn timestamp(mut self, t: u64) -> Self { + self.timestamp = t; + self + } + + /// Add a metadata key-value pair. + pub fn add_metadata(mut self, key: impl Into, value: MetadataValue) -> Self { + self.metadata.push((key.into(), value)); + self + } + + pub(crate) fn into_commit( + self, + root_tree: Sha256Digest, + root_metadata: Sha256Digest, + ) -> OstreeCommit { + OstreeCommit { + parent_commit: self.parent_commit, + metadata: self.metadata, + subject: self.subject, + body: self.body, + timestamp: self.timestamp, + root_tree, + root_metadata, + } + } +} + /// Decoded ostree commit object with metadata, tree root, and optional parent. #[derive(Debug)] pub struct OstreeCommit { /// Checksum of the parent commit, if any. pub parent_commit: Option, - /// Commit metadata as (key, value) string pairs. - pub metadata: Vec<(String, String)>, + /// Commit metadata as (key, value) pairs with preserved variant types. + pub metadata: Vec<(String, MetadataValue)>, /// One-line commit subject. pub subject: String, /// Extended commit description. @@ -369,35 +476,147 @@ pub struct OstreeCommit { pub root_metadata: Sha256Digest, } -fn format_variant(v: &gvariant::Variant) -> String { - if let Some(s) = v.get(gv!("s")) { - return s.to_str().to_string(); - } - if let Some(b) = v.get(gv!("b")) { - return bool::from(*b).to_string(); +impl OstreeCommit { + /// Extract the caller-controlled fields for re-creating this commit. + pub fn commit_metadata(&self) -> CommitMetadata { + CommitMetadata { + parent_commit: self.parent_commit, + metadata: self.metadata.clone(), + subject: self.subject.clone(), + body: self.body.clone(), + timestamp: self.timestamp, + } } - if let Some(u) = v.get(gv!("u")) { - return u.to_string(); +} + +/// A typed value from an ostree commit metadata dictionary (`a{sv}`). +#[derive(Debug)] +pub enum MetadataValue { + /// GVariant type `s`. + String(String), + /// GVariant type `b`. + Bool(bool), + /// GVariant type `u`. + Uint32(u32), + /// GVariant type `t`. + Uint64(u64), + /// GVariant type `as`. + StringArray(Vec), + /// GVariant type `ay`. + ByteArray(Vec), + /// Raw GVariant variant data for types not handled above. + Other(Owned), +} + +impl Clone for MetadataValue { + fn clone(&self) -> Self { + match self { + Self::String(s) => Self::String(s.clone()), + Self::Bool(b) => Self::Bool(*b), + Self::Uint32(u) => Self::Uint32(*u), + Self::Uint64(t) => Self::Uint64(*t), + Self::StringArray(a) => Self::StringArray(a.clone()), + Self::ByteArray(a) => Self::ByteArray(a.clone()), + Self::Other(v) => { + let v: &Variant = v; + Self::Other(v.to_owned()) + } + } } - if let Some(t) = v.get(gv!("t")) { - return t.to_string(); +} + +impl MetadataValue { + fn from_variant(v: &Variant) -> Self { + if let Some(s) = v.get(gv!("s")) { + return Self::String(s.to_str().to_string()); + } + if let Some(b) = v.get(gv!("b")) { + return Self::Bool(bool::from(*b)); + } + if let Some(u) = v.get(gv!("u")) { + return Self::Uint32(*u); + } + if let Some(t) = v.get(gv!("t")) { + return Self::Uint64(*t); + } + if let Some(arr) = v.get(gv!("as")) { + return Self::StringArray(arr.iter().map(|s| s.to_str().to_string()).collect()); + } + if let Some(ay) = v.get(gv!("ay")) { + return Self::ByteArray(ay.to_vec()); + } + Self::Other(v.to_owned()) } - if let Some(arr) = v.get(gv!("as")) { - let items: Vec<&str> = arr.iter().map(|s| s.to_str()).collect(); - return format!("[{}]", items.join(", ")); +} + +impl fmt::Display for MetadataValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::String(s) => write!(f, "{s}"), + Self::Bool(b) => write!(f, "{b}"), + Self::Uint32(u) => write!(f, "{u}"), + Self::Uint64(t) => write!(f, "{t}"), + Self::StringArray(arr) => { + let items: Vec<&str> = arr.iter().map(|s| s.as_str()).collect(); + write!(f, "[{}]", items.join(", ")) + } + Self::ByteArray(ay) => write!(f, "{}", hex::encode(ay)), + Self::Other(v) => { + let (typestr, data) = v.split(); + write!( + f, + "<{}:{}>", + std::str::from_utf8(typestr).unwrap_or("?"), + hex::encode(data) + ) + } + } } - if let Some(ay) = v.get(gv!("ay")) { - return hex::encode(ay); +} + +impl SerializeTo for &MetadataValue { + fn serialize(self, f: &mut impl Write) -> std::io::Result { + match self { + MetadataValue::String(s) => VariantWrap(gv!("s"), s.as_str()).serialize(f), + MetadataValue::Bool(b) => VariantWrap(gv!("b"), b).serialize(f), + MetadataValue::Uint32(u) => VariantWrap(gv!("u"), *u).serialize(f), + MetadataValue::Uint64(t) => VariantWrap(gv!("t"), *t).serialize(f), + MetadataValue::StringArray(arr) => { + let refs: Vec<&str> = arr.iter().map(|s| s.as_str()).collect(); + VariantWrap(gv!("as"), refs).serialize(f) + } + MetadataValue::ByteArray(ay) => VariantWrap(gv!("ay"), ay.as_slice()).serialize(f), + MetadataValue::Other(v) => { + let v: &Variant = v; + v.serialize(f) + } + } } - let (typestr, data) = v.split(); - format!( - "<{}:{}>", - std::str::from_utf8(typestr).unwrap_or("?"), - hex::encode(data) - ) } impl OstreeCommit { + /// Serialize to raw gvariant data. + pub fn serialize(&self) -> Vec { + let metadata: Vec<(&str, &MetadataValue)> = + self.metadata.iter().map(|(k, v)| (k.as_str(), v)).collect(); + let parent: &[u8] = self + .parent_commit + .as_ref() + .map(|c| c.as_slice()) + .unwrap_or(&[]); + let related: Vec<(&str, [u8; 32])> = vec![]; + gv!("(a{sv}aya(say)sstayay)").serialize_to_vec(&( + &metadata, + parent, + &related, + self.subject.as_str(), + self.body.as_str(), + u64::to_be(self.timestamp), + self.root_tree.as_slice(), + self.root_metadata.as_slice(), + )) + } + /// Deserialize from raw gvariant data. pub fn from_data(data: &AlignedSlice) -> Result { let gv = gv!("(a{sv}aya(say)sstayay)").cast(data.as_aligned()); @@ -418,7 +637,7 @@ impl OstreeCommit { .iter() .map(|entry| { let (key, value) = entry.to_tuple(); - (key.to_str().to_string(), format_variant(value)) + (key.to_str().to_string(), MetadataValue::from_variant(value)) }) .collect(); diff --git a/crates/composefs/src/generic_tree.rs b/crates/composefs/src/generic_tree.rs index 5917d550..e771d3aa 100644 --- a/crates/composefs/src/generic_tree.rs +++ b/crates/composefs/src/generic_tree.rs @@ -97,6 +97,19 @@ pub enum LeafContent { } impl LeafContent { + /// Returns the `S_IFMT` file-type bits for this content type. + pub fn file_type_bits(&self) -> u32 { + use crate::erofs::format; + match self { + Self::Regular(_) => format::S_IFREG as u32, + Self::Symlink(_) => format::S_IFLNK as u32, + Self::BlockDevice(_) => format::S_IFBLK as u32, + Self::CharacterDevice(_) => format::S_IFCHR as u32, + Self::Fifo => format::S_IFIFO as u32, + Self::Socket => format::S_IFSOCK as u32, + } + } + /// Maps `Regular(&T)` to `Regular(U)` via a fallible function, /// passing all other variants through unchanged. pub fn try_map_ref( diff --git a/crates/composefs/src/tree.rs b/crates/composefs/src/tree.rs index 59b2ee32..190c3f85 100644 --- a/crates/composefs/src/tree.rs +++ b/crates/composefs/src/tree.rs @@ -28,6 +28,16 @@ pub enum RegularFile { Sparse(u64), } +impl RegularFile { + /// Returns the file size in bytes. + pub fn file_size(&self) -> u64 { + match self { + Self::Inline(data) => data.len() as u64, + Self::External(_, size) | Self::ExternalNoVerity(_, size) | Self::Sparse(size) => *size, + } + } +} + // Re-export generic types. Note that we don't need to re-write // the generic constraint T: FsVerityHashValue here because it will // be transitively enforced.