use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Write};
use ferrosys::ext::ondisk::{Inode, Timestamp, Xattr};
use ferrosys::ext::{Acl, ReadPolicy, Reader, WalkEntry};
use tar::{Builder, EntryType, Header};
use crate::args::{ExtractArgs, ExtractMode, Stream};
use crate::json::Obj;
use crate::{Error, emit, from_read, render};
const LOST_FOUND: &[u8] = b"/lost+found";
const ROOT_INO: u32 = 2;
const IFMT: u16 = 0o170000;
const IFDIR: u16 = 0o040000;
const IFREG: u16 = 0o100000;
const IFLNK: u16 = 0o120000;
const IFCHR: u16 = 0o020000;
const IFBLK: u16 = 0o060000;
const IFIFO: u16 = 0o010000;
const IFSOCK: u16 = 0o140000;
pub fn run(args: ExtractArgs) -> Result<(), Error> {
let image = args.image.display().to_string();
let file = File::open(&args.image).map_err(|e| Error::io(&args.image, e))?;
let mut reader = Reader::open_at(file, args.offset, ReadPolicy::Lenient).map_err(|source| {
Error::NotExt {
path: image,
source,
}
})?;
match args.mode {
ExtractMode::Cat(path) => cat(&mut reader, &path),
ExtractMode::List { json } => list(&mut reader, json),
ExtractMode::ToTar(Stream::Std) => {
let stdout = io::stdout();
let mut out = stdout.lock();
to_tar(&mut reader, &mut out)?;
out.flush().map_err(|source| Error::Io {
what: "standard output".to_string(),
source,
})
}
ExtractMode::ToTar(Stream::File(path)) => {
let file = File::create(&path).map_err(|e| Error::io(&path, e))?;
let mut out = io::BufWriter::new(file);
to_tar(&mut reader, &mut out)?;
out.flush().map_err(|e| Error::io(&path, e))
}
}
}
struct Member {
path: Vec<u8>,
inode: Inode,
xattrs: Vec<Xattr>,
hardlink: Option<Vec<u8>>,
symlink: Option<Vec<u8>>,
device: Option<(u32, u32)>,
data: Vec<u8>,
}
fn is_lost_found(path: &[u8]) -> bool {
path == LOST_FOUND || path.starts_with(b"/lost+found/")
}
fn member_name(path: &[u8], directory: bool) -> Vec<u8> {
let mut name = Vec::with_capacity(path.len() + 2);
name.push(b'.');
name.extend_from_slice(path);
if directory {
name.push(b'/');
}
name
}
fn to_tar(reader: &mut Reader<File>, out: impl Write) -> Result<(), Error> {
let entries = reader.walk().map_err(from_read)?;
let mut builder = Builder::new(out);
let root = reader.inode(ROOT_INO).map_err(from_read)?;
let root = Member {
path: Vec::new(),
xattrs: reader.xattrs(&root).map_err(from_read)?,
inode: root,
hardlink: None,
symlink: None,
device: None,
data: Vec::new(),
};
append(&mut builder, &root)?;
let mut named: HashMap<u32, Vec<u8>> = HashMap::new();
for entry in entries {
if is_lost_found(&entry.path) {
continue;
}
append(&mut builder, &member(reader, entry, &mut named)?)?;
}
builder.finish().map_err(archive_write)
}
fn member(
reader: &mut Reader<File>,
entry: WalkEntry,
named: &mut HashMap<u32, Vec<u8>>,
) -> Result<Member, Error> {
let WalkEntry {
path,
number,
inode,
} = entry;
let kind = inode.mode & IFMT;
if kind == IFSOCK {
return Err(Error::Unrepresentable(path));
}
let hardlink = if kind == IFDIR {
None
} else {
match named.get(&number) {
Some(first) => Some(member_name(first, false)),
None => {
named.insert(number, path.clone());
None
}
}
};
if hardlink.is_some() {
return Ok(Member {
path,
inode,
xattrs: Vec::new(),
hardlink,
symlink: None,
device: None,
data: Vec::new(),
});
}
let symlink = if kind == IFLNK {
Some(reader.read_symlink(&inode).map_err(from_read)?)
} else {
None
};
let device = if kind == IFCHR || kind == IFBLK {
Some(reader.device(&inode))
} else {
None
};
let data = if kind == IFREG {
reader.read_data(&inode).map_err(from_read)?
} else {
Vec::new()
};
let xattrs = reader.xattrs(&inode).map_err(from_read)?;
Ok(Member {
path,
inode,
xattrs,
hardlink: None,
symlink,
device,
data,
})
}
fn archive_write(source: io::Error) -> Error {
Error::Io {
what: "the archive".to_string(),
source,
}
}
fn append(builder: &mut Builder<impl Write>, m: &Member) -> Result<(), Error> {
let kind = m.inode.mode & IFMT;
let name = member_name(&m.path, kind == IFDIR);
let entry_type = if m.hardlink.is_some() {
EntryType::Link
} else {
match kind {
IFDIR => EntryType::Directory,
IFREG => EntryType::Regular,
IFLNK => EntryType::Symlink,
IFCHR => EntryType::Char,
IFBLK => EntryType::Block,
IFIFO => EntryType::Fifo,
_ => return Err(Error::Unrepresentable(m.path.clone())),
}
};
let target = m.hardlink.as_ref().or(m.symlink.as_ref());
let mut records: Vec<(String, Vec<u8>)> = vec![
("path".to_string(), name.clone()),
("atime".to_string(), render::pax_time(m.inode.atime).into()),
("ctime".to_string(), render::pax_time(m.inode.ctime).into()),
("mtime".to_string(), render::pax_time(m.inode.mtime).into()),
("uid".to_string(), m.inode.uid.to_string().into()),
("gid".to_string(), m.inode.gid.to_string().into()),
];
if let Some(target) = target {
records.push(("linkpath".to_string(), target.clone()));
}
for xattr in &m.xattrs {
records.push((
pax_xattr_key(&m.path, &xattr.name)?,
xattr_value(&m.path, xattr)?,
));
}
let borrowed: Vec<(&str, &[u8])> = records
.iter()
.map(|(k, v)| (k.as_str(), v.as_slice()))
.collect();
builder
.append_pax_extensions(borrowed)
.map_err(archive_write)?;
let mut header = Header::new_ustar();
header.set_entry_type(entry_type);
header.set_mode(u32::from(m.inode.mode & 0o7777));
header.set_uid(u64::from(m.inode.uid));
header.set_gid(u64::from(m.inode.gid));
header.set_mtime(u64::try_from(m.inode.mtime.secs).unwrap_or(0));
if let Some((major, minor)) = m.device {
header.set_device_major(major).map_err(archive_write)?;
header.set_device_minor(minor).map_err(archive_write)?;
}
if let Ok(text) = std::str::from_utf8(&name) {
let _ = header.set_path(text);
}
if let Some(target) = target
&& let Ok(text) = std::str::from_utf8(target)
{
let _ = header.set_link_name(text);
}
header.set_size(m.data.len() as u64);
header.set_cksum();
builder
.append(&header, m.data.as_slice())
.map_err(archive_write)
}
fn pax_xattr_key(path: &[u8], name: &[u8]) -> Result<String, Error> {
match std::str::from_utf8(name) {
Ok(text) if !text.contains('=') && !text.contains('\n') => {
Ok(format!("SCHILY.xattr.{text}"))
}
_ => Err(Error::XattrNameUnrepresentable {
path: path.to_vec(),
name: name.to_vec(),
}),
}
}
fn xattr_value(path: &[u8], xattr: &Xattr) -> Result<Vec<u8>, Error> {
if xattr.name == Acl::ACCESS_NAME || xattr.name == Acl::DEFAULT_NAME {
let acl = Acl::decode(&xattr.value).map_err(|source| Error::BadAcl {
path: path.to_vec(),
source,
})?;
return Ok(acl.encode_xattr_v2());
}
Ok(xattr.value.clone())
}
fn cat(reader: &mut Reader<File>, path: &[u8]) -> Result<(), Error> {
let (_, inode) = reader.lookup(path).map_err(from_read)?;
if inode.mode & IFMT != IFREG {
return Err(Error::NotAFile(path.to_vec()));
}
let data = reader.read_data(&inode).map_err(from_read)?;
emit(&data)
}
fn list(reader: &mut Reader<File>, as_json: bool) -> Result<(), Error> {
let entries = reader.walk().map_err(from_read)?;
let mut targets: HashMap<usize, Vec<u8>> = HashMap::new();
for (i, e) in entries.iter().enumerate() {
if e.inode.mode & IFMT == IFLNK {
targets.insert(i, reader.read_symlink(&e.inode).map_err(from_read)?);
}
}
let text = if as_json {
list_json(&entries, &targets)
} else {
list_table(&entries, &targets)
};
emit(text.as_bytes())
}
fn list_table(entries: &[WalkEntry], targets: &HashMap<usize, Vec<u8>>) -> String {
let mut s = String::new();
for (i, e) in entries.iter().enumerate() {
s.push_str(&format!(
"{} {:>3} {:>6} {:>6} {:>10} {} {}",
render::mode(e.inode.mode),
e.inode.links_count,
e.inode.uid,
e.inode.gid,
e.inode.size,
render::iso8601(e.inode.mtime.secs),
render::printable(&e.path),
));
if let Some(target) = targets.get(&i) {
s.push_str(" -> ");
s.push_str(&render::printable(target));
}
s.push('\n');
}
s
}
fn list_json(entries: &[WalkEntry], targets: &HashMap<usize, Vec<u8>>) -> String {
let mut out = String::new();
let mut o = Obj::new(&mut out);
o.u64("version", 1);
let mut a = o.arr("entries");
for (i, e) in entries.iter().enumerate() {
let mut j = a.obj();
j.bytes("path", &e.path);
j.u64("inode", u64::from(e.number));
j.str("type", kind_name(e.inode.mode));
j.u64("mode", u64::from(e.inode.mode & 0o7777));
j.u64("uid", u64::from(e.inode.uid));
j.u64("gid", u64::from(e.inode.gid));
j.u64("links", u64::from(e.inode.links_count));
j.u64("size", e.inode.size);
time(&mut j, "atime", e.inode.atime);
time(&mut j, "ctime", e.inode.ctime);
time(&mut j, "mtime", e.inode.mtime);
if let Some(target) = targets.get(&i) {
j.bytes("target", target);
}
j.end();
}
a.end();
o.end();
out.push('\n');
out
}
fn time(o: &mut Obj<'_>, key: &str, t: Timestamp) {
o.i64(key, t.secs);
o.u64(&format!("{key}_nanos"), u64::from(t.nanos));
}
fn kind_name(mode: u16) -> &'static str {
match mode & IFMT {
IFDIR => "directory",
IFREG => "file",
IFLNK => "symlink",
IFCHR => "char_device",
IFBLK => "block_device",
IFIFO => "fifo",
IFSOCK => "socket",
_ => "unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_member_is_named_relative_to_the_archive_root() {
assert_eq!(member_name(b"/etc/hostname", false), b"./etc/hostname");
assert_eq!(member_name(b"/etc", true), b"./etc/");
assert_eq!(member_name(b"", true), b"./");
}
#[test]
fn lost_and_found_is_recognized_with_its_contents() {
assert!(is_lost_found(b"/lost+found"));
assert!(is_lost_found(b"/lost+found/17"));
assert!(!is_lost_found(b"/lost+found-old"));
assert!(!is_lost_found(b"/etc/lost+found"));
}
#[test]
fn a_pax_xattr_key_is_built_for_a_plain_name_and_refused_otherwise() {
assert_eq!(
pax_xattr_key(b"/f", b"user.comment").unwrap(),
"SCHILY.xattr.user.comment"
);
for bad in [&b"user.a=b"[..], b"user.a\nb", b"user.\xff"] {
assert!(
matches!(
pax_xattr_key(b"/f", bad),
Err(Error::XattrNameUnrepresentable { .. })
),
"name {bad:?} must be refused"
);
}
}
}