Skip to content

Commit

Permalink
dumpfile: Add some test code to convert to tar
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
cgwalters committed Sep 4, 2024
1 parent 47042e8 commit 5180b3a
Showing 1 changed file with 117 additions and 2 deletions.
119 changes: 117 additions & 2 deletions rust/composefs/src/dumpfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<W: Write>(e: &Entry, w: &mut tar::Builder<W>) -> 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() {
Expand Down

0 comments on commit 5180b3a

Please sign in to comment.