From 5180b3aba77826b297f19979ddbe8cefe33b663f Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 4 Sep 2024 14:18:01 -0400 Subject: [PATCH] dumpfile: Add some test code to convert to tar Add code to go from dumpfile -> tar (notably, ignoring non-inline regular file content). This is *testing* only code right now, though clearly mapping to/from tar would be very useful in general. I plan to use this code for various testing purposes especially to *manually* create tarballs from various dumpfiles to e.g. feed directly to `mkfs.erofs` for example. But we at least get some sanity checking this way. Signed-off-by: Colin Walters --- rust/composefs/src/dumpfile.rs | 119 ++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/rust/composefs/src/dumpfile.rs b/rust/composefs/src/dumpfile.rs index 35f7d7cf..aa8d732f 100644 --- a/rust/composefs/src/dumpfile.rs +++ b/rust/composefs/src/dumpfile.rs @@ -538,14 +538,129 @@ where #[cfg(test)] mod tests { use std::fs::File; - - use crate::mkcomposefs::{mkcomposefs_from_buf, Config}; + use std::io::{BufRead, BufReader, BufWriter, Cursor, Read}; use super::*; + use crate::mkcomposefs::{mkcomposefs_from_buf, Config}; const SPECIAL_DUMP: &str = include_str!("../../../tests/assets/special.dump"); const SPECIALS: &[&str] = &["", "foo=bar=baz", r"\x01\x02", "-"]; const UNQUOTED: &[&str] = &["foo!bar", "hello-world", "--"]; + const PAX_SCHILY_XATTR: &[u8] = b"SCHILY.xattr."; + + /// Convert an input path to relative as it's what tar wants. + fn make_relative(p: &Path) -> &Path { + let p = p.strip_prefix("/").unwrap_or(p); + // Special case `/` -> `.` + // All other paths just have the leading `/` removed. + if p.as_os_str().is_empty() { + Path::new(".") + } else { + p + } + } + + fn entry_to_tar(e: &Entry, w: &mut tar::Builder) -> Result<()> { + let path = make_relative(&e.path); + let mut h = tar::Header::new_ustar(); + let fmt = e.mode & libc::S_IFMT; + h.set_mode(e.mode); + h.set_uid(e.uid.into()); + h.set_gid(e.gid.into()); + // Discard nanos currently + h.set_mtime(e.mtime.sec); + match e.xattrs.as_slice() { + [] => {} + xattrs => { + let mut pax_header = tar::Header::new_ustar(); + let mut pax_data = Vec::new(); + for xattr in xattrs { + let key = xattr.key.as_bytes(); + let value = &xattr.value; + let data_len = PAX_SCHILY_XATTR.len() + key.len() + value.len() + 3; + // Calculate the total length, including the length of the length field + let mut len_len = 1; + while data_len + len_len >= 10usize.pow(len_len.try_into().unwrap()) { + len_len += 1; + } + write!(pax_data, "{} ", data_len + len_len)?; + pax_data.write_all(PAX_SCHILY_XATTR)?; + pax_data.write_all(key)?; + pax_data.write_all(b"=")?; + pax_data.write_all(&value)?; + pax_data.write_all(b"\n")?; + } + if !pax_data.is_empty() { + pax_header.set_size(pax_data.len() as u64); + pax_header.set_entry_type(tar::EntryType::XHeader); + pax_header.set_cksum(); + w.append(&pax_header, &*pax_data)?; + } + } + } + match &e.item { + Item::Regular { inline_content, .. } => { + h.set_entry_type(tar::EntryType::Regular); + if let Some(inline_content) = inline_content.as_deref() { + h.set_size(inline_content.len().try_into()?); + w.append_data(&mut h, path, inline_content)?; + } else { + h.set_size(0); + } + } + Item::Device { rdev, .. } => { + if fmt == libc::S_IFBLK { + h.set_entry_type(tar::EntryType::Block); + } else if fmt == libc::S_IFCHR { + h.set_entry_type(tar::EntryType::Char) + } else { + panic!("Unhandled mode for device entry: {e}"); + } + let major = (rdev << 4) & 0xFF; + h.set_device_major(major)?; + h.set_device_minor(rdev & 0xFF)?; + } + Item::Symlink { target, .. } => { + h.set_entry_type(tar::EntryType::Symlink); + w.append_link(&mut h, path, target)?; + } + Item::Hardlink { target } => { + h.set_entry_type(tar::EntryType::Link); + w.append_link(&mut h, path, target)?; + } + Item::Fifo { .. } => { + h.set_entry_type(tar::EntryType::Fifo); + w.append_data(&mut h, path, std::io::empty())?; + } + Item::Directory { .. } => { + h.set_entry_type(tar::EntryType::Directory); + w.append_data(&mut h, path, std::io::empty())?; + } + } + + Ok(()) + } + + fn dumpfile_to_tar(src: impl Read, dst: impl Write) -> Result<()> { + let src = BufReader::new(src); + let dst = BufWriter::new(dst); + let mut dst = tar::Builder::new(dst); + for line in src.lines() { + let line = line?; + let entry = Entry::parse(&line)?; + entry_to_tar(&entry, &mut dst).with_context(|| format!("Processing entry: {entry}"))?; + } + dst.into_inner()?.into_inner().map_err(|e| e.into_error())?; + Ok(()) + } + + #[test] + fn test_dumpfile_to_tar() -> Result<()> { + const CONTENT: &str = include_str!("../../../tests/assets/special.dump"); + let mut dst = Vec::new(); + dumpfile_to_tar(Cursor::new(CONTENT), &mut dst).unwrap(); + Ok(()) + } #[test] fn test_escape_roundtrip() {