use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Write};
use std::path::Path;
#[cfg(any(target_os = "linux", target_os = "android"))]
use ferrosys::ext::DirectorySink;
use ferrosys::ext::ondisk::{Inode, Timestamp};
use ferrosys::ext::{Acl, ArchiveSink, Limits, OpenOptions, ReadPolicy, Reader, WalkEntry, Xattr};
use crate::args::{ExtractArgs, ExtractMode, Stream};
use crate::dest::Destination;
use crate::json::Obj;
use crate::{Error, emit, from_read, render};
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 limits = Limits::new();
if let Some(max) = args.max_file_bytes {
limits = limits.max_file_bytes(max);
}
let mut reader = Reader::open_with(
file,
&OpenOptions::new()
.base(args.offset)
.policy(ReadPolicy::Lenient)
.limits(limits),
)
.map_err(|source| Error::NotExt {
path: image,
source,
})?;
match args.mode {
ExtractMode::Cat(path) => cat(&mut reader, &path),
ExtractMode::Stat { path, json } => stat(&mut reader, &path, json),
ExtractMode::List { json } => list(&mut reader, json),
ExtractMode::ToTar(Stream::Std) => {
let stdout = io::stdout();
let mut out = stdout.lock();
ArchiveSink::new(&mut out).write_tree(&mut reader)?;
out.flush().map_err(|source| Error::Io {
what: "standard output".to_string(),
source,
})
}
ExtractMode::ToTar(Stream::File(path)) => {
let mut dest = Destination::open(&path, args.atomic)?;
let mut out = io::BufWriter::new(dest.file());
ArchiveSink::new(&mut out).write_tree(&mut reader)?;
out.flush().map_err(|e| Error::io(&path, e))?;
drop(out);
dest.commit()
}
ExtractMode::ToDir {
path,
skip_privileged,
} => to_dir(&mut reader, &path, skip_privileged),
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn to_dir(reader: &mut Reader<File>, path: &Path, skip_privileged: bool) -> Result<(), Error> {
if !path.exists() {
std::fs::create_dir_all(path).map_err(|e| Error::io(path, e))?;
}
let mut sink = DirectorySink::new(path)?;
if skip_privileged {
sink = sink.skip_privileged();
}
let report = sink.write_tree(reader)?;
eprintln!("{:<24}{}", "Names written:", report.written);
if report.ownership_dropped {
eprintln!(
"{:<24}not applied — this process may not set another owner",
"Ownership:"
);
}
if report.xattrs_dropped {
eprintln!(
"{:<24}not applied — this process may not set security or trusted attributes",
"Attributes:"
);
}
for skipped in &report.skipped {
eprintln!("{:<24}{}", "Skipped:", render::printable(skipped));
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn to_dir(_reader: &mut Reader<File>, _path: &Path, _skip: bool) -> Result<(), Error> {
Err(Error::NoDirectorySink)
}
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 stdout = io::stdout();
let mut out = stdout.lock();
reader.read_data_to(&inode, &mut out).map_err(from_read)?;
out.flush().map_err(|source| Error::Io {
what: "standard output".to_string(),
source,
})
}
fn stat(reader: &mut Reader<File>, path: &[u8], as_json: bool) -> Result<(), Error> {
let (number, inode) = reader.lookup_no_follow(path).map_err(from_read)?;
let xattrs = reader.xattrs(&inode).map_err(from_read)?;
let target = if inode.mode & IFMT == IFLNK {
Some(reader.read_symlink(&inode).map_err(from_read)?)
} else {
None
};
let device = match inode.mode & IFMT {
IFCHR | IFBLK => Some(reader.device(&inode)),
_ => None,
};
let text = if as_json {
stat_json(path, number, &inode, &xattrs, target.as_deref(), device)
} else {
stat_table(path, number, &inode, &xattrs, target.as_deref(), device)
};
emit(text.as_bytes())
}
fn stat_table(
path: &[u8],
number: u32,
inode: &Inode,
xattrs: &[Xattr],
target: Option<&[u8]>,
device: Option<(u32, u32)>,
) -> String {
let mut s = String::new();
let mut line = |k: &str, v: String| {
s.push_str(&format!("{k:<24}{v}\n"));
};
line("Path:", render::printable(path));
line("Inode:", number.to_string());
line("Type:", kind_name(inode.mode).to_string());
line(
"Mode:",
format!("{:04o} ({})", inode.mode & 0o7777, render::mode(inode.mode)),
);
line("Owner:", format!("{}:{}", inode.uid, inode.gid));
line("Links:", inode.links_count.to_string());
line("Size:", inode.size.to_string());
line("Blocks:", inode.blocks.to_string());
if let Some((major, minor)) = device {
line("Device:", format!("{major}:{minor}"));
}
if let Some(target) = target {
line("Symlink target:", render::printable(target));
}
for (label, t) in [
("Accessed:", inode.atime),
("Modified:", inode.mtime),
("Changed:", inode.ctime),
("Created:", inode.crtime),
] {
line(
label,
format!("{} ({} ns)", render::iso8601(t.secs), t.nanos),
);
}
for xattr in xattrs {
let rendered = acl_text(xattr).unwrap_or_else(|| render::printable(&xattr.value));
line(
&format!("Xattr {}:", render::printable(&xattr.name)),
rendered,
);
}
s
}
fn stat_json(
path: &[u8],
number: u32,
inode: &Inode,
xattrs: &[Xattr],
target: Option<&[u8]>,
device: Option<(u32, u32)>,
) -> String {
let mut out = String::new();
let mut o = Obj::new(&mut out);
o.u64("schema", crate::json::SCHEMA_VERSION);
let mut e = o.obj("entry");
entry_fields(&mut e, path, number, inode, target);
e.u64("blocks", inode.blocks);
time(&mut e, "crtime", inode.crtime);
if let Some((major, minor)) = device {
let mut d = e.obj("device");
d.u64("major", u64::from(major));
d.u64("minor", u64::from(minor));
d.end();
}
xattr_fields(&mut e, xattrs);
e.end();
o.end();
out.push('\n');
out
}
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();
let mut xattrs: HashMap<usize, Vec<Xattr>> = 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)?);
}
if as_json {
let attrs = reader.xattrs(&e.inode).map_err(from_read)?;
if !attrs.is_empty() {
xattrs.insert(i, attrs);
}
}
}
let text = if as_json {
list_json(&entries, &targets, &xattrs)
} 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>>,
xattrs: &HashMap<usize, Vec<Xattr>>,
) -> String {
let mut out = String::new();
let mut o = Obj::new(&mut out);
o.u64("schema", crate::json::SCHEMA_VERSION);
let mut a = o.arr("entries");
for (i, e) in entries.iter().enumerate() {
let mut j = a.obj();
entry_fields(
&mut j,
&e.path,
e.number,
&e.inode,
targets.get(&i).map(Vec::as_slice),
);
if let Some(attrs) = xattrs.get(&i) {
xattr_fields(&mut j, attrs);
}
j.end();
}
a.end();
o.end();
out.push('\n');
out
}
fn entry_fields(o: &mut Obj<'_>, path: &[u8], number: u32, inode: &Inode, target: Option<&[u8]>) {
o.bytes("path", path);
o.u64("inode", u64::from(number));
o.str("type", kind_name(inode.mode));
o.u64("mode", u64::from(inode.mode & 0o7777));
o.str("mode_octal", &format!("{:04o}", inode.mode & 0o7777));
o.u64("uid", u64::from(inode.uid));
o.u64("gid", u64::from(inode.gid));
o.u64("links", u64::from(inode.links_count));
o.u64("size", inode.size);
time(o, "atime", inode.atime);
time(o, "ctime", inode.ctime);
time(o, "mtime", inode.mtime);
if let Some(target) = target {
o.bytes("target", target);
}
}
fn xattr_fields(o: &mut Obj<'_>, xattrs: &[Xattr]) {
let mut a = o.arr("xattrs");
for xattr in xattrs {
let mut x = a.obj();
x.bytes("name", &xattr.name);
x.bytes("value", &xattr.value);
if let Some(text) = acl_text(xattr) {
x.str("acl", &text);
}
x.end();
}
a.end();
}
fn acl_text(xattr: &Xattr) -> Option<String> {
if xattr.name != Acl::ACCESS_NAME && xattr.name != Acl::DEFAULT_NAME {
return None;
}
Acl::decode(&xattr.value).ok().map(|acl| render::acl(&acl))
}
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",
}
}