use std::io::{IsTerminal, Read, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use crate::Result;
use crate::block::BlockDevice;
use crate::compression::Algo;
use crate::fs::ext::xattr::Xattr;
use crate::fs::ext::{Ext, FsKind};
use crate::fs::tar::{TarEntryMeta, TarStreamWriter};
use crate::fs::{DeviceKind, FileMeta, Filesystem, XattrPair};
pub struct Progress {
files: u64,
last_emit: Instant,
last_path: String,
is_tty: bool,
verbose: bool,
started: Instant,
total_files: Option<u64>,
total_bytes: Option<u64>,
bytes_done: u64,
}
impl Progress {
pub fn auto() -> Self {
let now = Instant::now();
Self {
files: 0,
last_emit: now - Duration::from_secs(1),
last_path: String::new(),
is_tty: std::io::stderr().is_terminal(),
verbose: false,
started: now,
total_files: None,
total_bytes: None,
bytes_done: 0,
}
}
pub fn verbose() -> Self {
let mut p = Self::auto();
p.verbose = true;
p
}
fn note_inner(&mut self, path: &str) {
self.files += 1;
self.last_path.clear();
self.last_path.push_str(path);
self.emit_status();
}
fn note_bytes_inner(&mut self, n: u64) {
self.bytes_done = self.bytes_done.saturating_add(n);
self.emit_status();
}
fn emit_status(&mut self) {
let now = Instant::now();
if now.duration_since(self.last_emit) < Duration::from_millis(200) {
return;
}
self.last_emit = now;
let cols = if self.is_tty { term_cols() } else { None };
let line = self.status_line(cols);
if self.is_tty {
let mut err = std::io::stderr().lock();
let _ = write!(err, "\r\x1b[Krepack: {line}");
let _ = err.flush();
} else if self.verbose && self.files % 500 == 0 {
eprintln!("repack: {line}");
}
}
fn status_line(&self, cols: Option<usize>) -> String {
match (self.total_files, self.total_bytes) {
(Some(tf), Some(tb)) if tf > 0 || tb > 0 => {
let file_frac = if tf > 0 {
(self.files as f64 / tf as f64).clamp(0.0, 1.0)
} else {
0.0
};
let byte_frac = if tb > 0 {
(self.bytes_done as f64 / tb as f64).clamp(0.0, 1.0)
} else {
0.0
};
let frac = if tb > 0 { byte_frac } else { file_frac };
let bar = render_bar(24, frac);
format!(
"{bar} {:3.0}% {}/{} files {}/{}",
frac * 100.0,
self.files,
tf,
human_bytes(self.bytes_done),
human_bytes(tb),
)
}
_ => {
let prefix = format!("{} files | ", self.files);
let path = match cols {
Some(c) => {
let budget = c.saturating_sub(8 + prefix.chars().count() + 1);
truncate_left(&self.last_path, budget)
}
None => self.last_path.clone(),
};
format!("{prefix}{path}")
}
}
}
fn phase_inner(&mut self, msg: &str) {
self.last_emit = Instant::now();
if self.is_tty {
let mut err = std::io::stderr().lock();
let _ = writeln!(err, "\r\x1b[Krepack: {msg}");
let _ = err.flush();
} else {
eprintln!("repack: {msg}");
}
}
fn finish_inner(&self) {
let elapsed = self.started.elapsed();
if self.is_tty {
let mut err = std::io::stderr().lock();
let _ = writeln!(
err,
"\r\x1b[Krepack: {} files in {:.1}s",
self.files,
elapsed.as_secs_f32()
);
} else if self.verbose || self.files > 0 {
eprintln!(
"repack: {} files in {:.1}s",
self.files,
elapsed.as_secs_f32()
);
}
}
}
#[cfg(unix)]
fn term_cols() -> Option<usize> {
use std::os::fd::AsRawFd;
let fd = std::io::stderr().as_raw_fd();
let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) };
if rc != 0 || ws.ws_col == 0 {
return None;
}
Some(ws.ws_col as usize)
}
#[cfg(not(unix))]
fn term_cols() -> Option<usize> {
None
}
fn truncate_left(path: &str, budget: usize) -> String {
if budget == 0 {
return String::new();
}
if path.chars().count() <= budget {
return path.to_string();
}
let keep = budget.saturating_sub(1);
let (byte_start, _) = path
.char_indices()
.rev()
.nth(keep.saturating_sub(1))
.unwrap_or((path.len(), '\0'));
let mut out = String::with_capacity(1 + path.len() - byte_start);
out.push('…');
out.push_str(&path[byte_start..]);
out
}
fn render_bar(cells: usize, frac: f64) -> String {
let filled = (cells as f64 * frac).round() as usize;
let filled = filled.min(cells);
let mut s = String::with_capacity(cells + 2);
s.push('[');
for i in 0..cells {
s.push(if i < filled { '█' } else { '░' });
}
s.push(']');
s
}
fn human_bytes(n: u64) -> String {
const KIB: f64 = 1024.0;
let f = n as f64;
if f < KIB {
return format!("{n} B");
}
let mib = f / KIB / KIB;
if mib < 1.0 {
return format!("{:.1} KiB", f / KIB);
}
let gib = mib / KIB;
if gib < 1.0 {
return format!("{mib:.1} MiB");
}
format!("{gib:.2} GiB")
}
thread_local! {
static ACTIVE_PROGRESS: std::cell::RefCell<Option<Progress>> =
const { std::cell::RefCell::new(None) };
}
pub fn enter(p: Progress) {
ACTIVE_PROGRESS.with(|cell| *cell.borrow_mut() = Some(p));
}
pub fn leave() {
ACTIVE_PROGRESS.with(|cell| {
if let Some(p) = cell.borrow().as_ref() {
p.finish_inner();
}
*cell.borrow_mut() = None;
});
}
pub fn note(path: &str) {
ACTIVE_PROGRESS.with(|cell| {
if let Some(p) = cell.borrow_mut().as_mut() {
p.note_inner(path);
}
});
}
pub fn phase(msg: &str) {
ACTIVE_PROGRESS.with(|cell| {
if let Some(p) = cell.borrow_mut().as_mut() {
p.files = 0;
p.last_path.clear();
p.bytes_done = 0;
p.total_files = None;
p.total_bytes = None;
p.phase_inner(msg);
}
});
}
pub fn note_bytes(n: u64) {
ACTIVE_PROGRESS.with(|cell| {
if let Some(p) = cell.borrow_mut().as_mut() {
p.note_bytes_inner(n);
}
});
}
pub fn set_total(files: u64, bytes: u64) {
ACTIVE_PROGRESS.with(|cell| {
if let Some(p) = cell.borrow_mut().as_mut() {
p.total_files = Some(files);
p.total_bytes = Some(bytes);
}
});
}
#[derive(Debug, Clone)]
pub enum Source {
HostDir(PathBuf),
TarArchive { path: PathBuf, codec: Option<Algo> },
Image(crate::inspect::Target),
Layered(Vec<Source>),
}
impl Source {
pub fn detect(spec: &str) -> Result<Self> {
if spec.contains('+') {
let parts: Vec<_> = spec
.split('+')
.filter(|p| !p.is_empty())
.map(Self::detect)
.collect::<Result<_>>()?;
if parts.len() > 1 {
return Ok(Self::Layered(parts));
}
if let Some(single) = parts.into_iter().next() {
return Ok(single);
}
}
let bare = match spec.rsplit_once(':') {
Some((head, tail)) if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) => {
head
}
_ => spec,
};
let bare_path = Path::new(bare);
if bare == spec
&& let Ok(meta) = std::fs::metadata(bare_path)
&& meta.is_dir()
{
return Ok(Self::HostDir(bare_path.to_path_buf()));
}
if let Some(codec) = tar_input_codec(spec) {
return Ok(Self::TarArchive {
path: bare_path.to_path_buf(),
codec: Some(codec),
});
}
if has_plain_tar_extension(bare_path) {
return Ok(Self::TarArchive {
path: bare_path.to_path_buf(),
codec: None,
});
}
Ok(Self::Image(crate::inspect::Target::parse(spec)))
}
}
fn has_plain_tar_extension(path: &Path) -> bool {
path.extension()
.and_then(|s| s.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("tar"))
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RepackMeta {
pub mode: u16,
pub uid: u32,
pub gid: u32,
pub mtime: u32,
pub atime: u32,
pub ctime: u32,
}
impl RepackMeta {
fn dir_default() -> Self {
Self {
mode: 0o755,
uid: 0,
gid: 0,
mtime: 0,
atime: 0,
ctime: 0,
}
}
fn to_file_meta(self) -> FileMeta {
FileMeta {
mode: self.mode,
uid: self.uid,
gid: self.gid,
mtime: self.mtime,
atime: self.atime,
ctime: self.ctime,
}
}
fn to_tar_meta(self) -> TarEntryMeta {
TarEntryMeta {
mode: self.mode,
uid: self.uid,
gid: self.gid,
mtime: u64::from(self.mtime),
uname: String::new(),
gname: String::new(),
}
}
}
pub trait RepackSink {
fn put_dir(&mut self, path: &str, meta: RepackMeta, xattrs: &[XattrPair]) -> Result<()>;
fn put_file(
&mut self,
path: &str,
body: &mut dyn Read,
len: u64,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()>;
fn put_symlink(
&mut self,
path: &str,
target: &str,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()>;
fn put_device(
&mut self,
path: &str,
kind: DeviceKind,
major: u32,
minor: u32,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()>;
fn put_hardlink(
&mut self,
path: &str,
target: &str,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<bool>;
fn materialise_copy(
&mut self,
_path: &str,
_target: &str,
_meta: RepackMeta,
_xattrs: &[XattrPair],
) -> Result<()> {
Err(crate::Error::Unsupported(
"repack: sink can't materialise a hard-link copy".into(),
))
}
fn finish(&mut self) -> Result<()>;
}
fn xattrs_to_tar(xattrs: &[XattrPair]) -> Vec<Xattr> {
xattrs
.iter()
.map(|x| Xattr {
name: x.name.clone(),
value: x.value.clone(),
})
.collect()
}
pub struct FsSink<'a> {
dst: &'a mut dyn Filesystem,
dev: &'a mut dyn BlockDevice,
lossy: bool,
}
impl<'a> FsSink<'a> {
pub fn new(dst: &'a mut dyn Filesystem, dev: &'a mut dyn BlockDevice) -> Self {
Self {
dst,
dev,
lossy: false,
}
}
pub fn lossy(mut self) -> Self {
self.lossy = true;
self
}
fn apply_xattrs(&mut self, path: &str, xattrs: &[XattrPair]) -> Result<()> {
if xattrs.is_empty() {
return Ok(());
}
match self.dst.set_xattrs(self.dev, Path::new(path), xattrs) {
Ok(()) => Ok(()),
Err(crate::Error::Unsupported(_)) => Ok(()),
Err(e) if self.lossy => {
eprintln!("repack: dropping xattrs on {path:?}: {e}");
Ok(())
}
Err(e) => Err(e),
}
}
fn lossy_ok(&self, path: &str, what: &str, r: Result<()>) -> Result<()> {
match r {
Ok(()) => Ok(()),
Err(e) if self.lossy => {
eprintln!("repack: dropping {what} {path:?} — destination can't represent it: {e}");
Ok(())
}
Err(e) => Err(e),
}
}
}
impl RepackSink for FsSink<'_> {
fn put_dir(&mut self, path: &str, meta: RepackMeta, xattrs: &[XattrPair]) -> Result<()> {
self.dst
.create_dir(self.dev, Path::new(path), meta.to_file_meta())?;
self.apply_xattrs(path, xattrs)
}
fn put_file(
&mut self,
path: &str,
body: &mut dyn Read,
len: u64,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()> {
self.dst.create_file_streaming(
self.dev,
Path::new(path),
body,
len,
meta.to_file_meta(),
)?;
self.apply_xattrs(path, xattrs)
}
fn put_symlink(
&mut self,
path: &str,
target: &str,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()> {
let r = self.dst.create_symlink(
self.dev,
Path::new(path),
Path::new(target),
meta.to_file_meta(),
);
self.lossy_ok(path, "symlink", r)?;
self.apply_xattrs(path, xattrs)
}
fn put_device(
&mut self,
path: &str,
kind: DeviceKind,
major: u32,
minor: u32,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()> {
let r = self.dst.create_device(
self.dev,
Path::new(path),
kind,
major,
minor,
meta.to_file_meta(),
);
self.lossy_ok(path, "device node", r)?;
self.apply_xattrs(path, xattrs)
}
fn put_hardlink(
&mut self,
path: &str,
target: &str,
_meta: RepackMeta,
_xattrs: &[XattrPair],
) -> Result<bool> {
match self
.dst
.hardlink(self.dev, Path::new(target), Path::new(path))
{
Ok(()) => Ok(true),
Err(crate::Error::Unsupported(_)) => Ok(false),
Err(e) => Err(e),
}
}
fn materialise_copy(
&mut self,
path: &str,
target: &str,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()> {
let mut buf = Vec::new();
{
let mut r = self.dst.read_file(self.dev, Path::new(target))?;
r.read_to_end(&mut buf).map_err(crate::Error::from)?;
}
let len = buf.len() as u64;
let mut cur = std::io::Cursor::new(buf);
self.dst.create_file_streaming(
self.dev,
Path::new(path),
&mut cur,
len,
meta.to_file_meta(),
)?;
self.apply_xattrs(path, xattrs)
}
fn finish(&mut self) -> Result<()> {
self.dst.flush(self.dev)
}
}
pub struct TarStreamSink {
writer: TarStreamWriter<Box<dyn Write>>,
}
impl TarStreamSink {
pub fn new(inner: Box<dyn Write>) -> Self {
Self {
writer: TarStreamWriter::new(inner),
}
}
pub fn bytes_written(&self) -> u64 {
self.writer.bytes_written()
}
}
fn tar_name(path: &str) -> &str {
path.trim_start_matches('/')
}
impl RepackSink for TarStreamSink {
fn put_dir(&mut self, path: &str, meta: RepackMeta, xattrs: &[XattrPair]) -> Result<()> {
self.writer
.add_dir(tar_name(path), meta.to_tar_meta(), &xattrs_to_tar(xattrs))
}
fn put_file(
&mut self,
path: &str,
body: &mut dyn Read,
len: u64,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()> {
self.writer.add_file(
tar_name(path),
body,
len,
meta.to_tar_meta(),
&xattrs_to_tar(xattrs),
)
}
fn put_symlink(
&mut self,
path: &str,
target: &str,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()> {
self.writer.add_symlink(
tar_name(path),
target,
meta.to_tar_meta(),
&xattrs_to_tar(xattrs),
)
}
fn put_device(
&mut self,
path: &str,
kind: DeviceKind,
major: u32,
minor: u32,
meta: RepackMeta,
xattrs: &[XattrPair],
) -> Result<()> {
self.writer.add_device(
tar_name(path),
kind,
major,
minor,
meta.to_tar_meta(),
&xattrs_to_tar(xattrs),
)
}
fn put_hardlink(
&mut self,
_path: &str,
_target: &str,
_meta: RepackMeta,
_xattrs: &[XattrPair],
) -> Result<bool> {
Ok(false)
}
fn finish(&mut self) -> Result<()> {
self.writer.finish()
}
}
pub fn walk_source_into_sink(source: &Source, sink: &mut dyn RepackSink) -> Result<()> {
match source {
Source::HostDir(p) => walk_host_dir(p, sink),
Source::TarArchive { path, codec } => {
let mut reader = open_tar_stream(path, *codec)?;
walk_tar_stream(&mut reader, sink)
}
Source::Image(target) => walk_image(target, sink),
Source::Layered(layers) => {
let model = crate::merge::MergeModel::build(layers)?;
model.walk_into_sink(layers, sink)
}
}
}
fn split_rdev(rdev: u32) -> (u32, u32) {
crate::fs::ext::inode::decode_devnum(rdev)
}
fn walk_image(target: &crate::inspect::Target, sink: &mut dyn RepackSink) -> Result<()> {
crate::inspect::with_target_device(target, |src_dev| {
let mut src_fs = crate::inspect::AnyFs::open(src_dev)?;
if let crate::inspect::AnyFs::Ext(ext) = &mut src_fs {
let _ = ext.replay_pending_journal(src_dev)?;
}
walk_anyfs(&mut src_fs, src_dev, sink)
})
}
pub fn walk_anyfs(
src_fs: &mut crate::inspect::AnyFs,
src_dev: &mut dyn BlockDevice,
sink: &mut dyn RepackSink,
) -> Result<()> {
use crate::fs::EntryKind;
let mut link_map: std::collections::HashMap<(u32, u32), String> =
std::collections::HashMap::new();
let mut stack: Vec<String> = vec!["/".to_string()];
while let Some(dir) = stack.pop() {
for e in src_fs.list(src_dev, &dir)? {
if e.name == "." || e.name == ".." || e.name == "lost+found" {
continue;
}
let child = join_fs_path(&dir, &e.name);
note(&child);
let child_path = Path::new(&child);
let attrs = src_fs.getattr(src_dev, child_path)?;
let xattrs = src_fs.list_xattrs(src_dev, child_path)?;
let meta = RepackMeta {
mode: attrs.mode,
uid: attrs.uid,
gid: attrs.gid,
mtime: attrs.mtime,
atime: attrs.atime,
ctime: attrs.ctime,
};
match attrs.kind {
EntryKind::Dir => {
sink.put_dir(&child, meta, &xattrs)?;
stack.push(child);
}
EntryKind::Regular => {
if attrs.inode != 0 && attrs.nlink > 1 {
let key = (attrs.inode, attrs.nlink);
if let Some(first) = link_map.get(&key) {
if sink.put_hardlink(&child, first, meta, &xattrs)? {
continue;
}
} else {
link_map.insert(key, child.clone());
}
}
let mut body = src_fs.open_body_reader(src_dev, &child)?;
sink.put_file(&child, &mut *body, attrs.size, meta, &xattrs)?;
note_bytes(attrs.size);
}
EntryKind::Symlink => {
let target = src_fs.read_symlink(src_dev, &child)?;
sink.put_symlink(&child, &target, meta, &xattrs)?;
}
EntryKind::Char | EntryKind::Block | EntryKind::Fifo | EntryKind::Socket => {
let (major, minor) = split_rdev(attrs.rdev);
let kind = match attrs.kind {
EntryKind::Char => DeviceKind::Char,
EntryKind::Block => DeviceKind::Block,
EntryKind::Fifo => DeviceKind::Fifo,
EntryKind::Socket => DeviceKind::Socket,
_ => unreachable!(),
};
sink.put_device(&child, kind, major, minor, meta, &xattrs)?;
}
EntryKind::Unknown => {
eprintln!("repack: skipping unknown entry {child:?}");
}
}
}
}
Ok(())
}
pub fn open_tar_stream(path: &Path, codec: Option<Algo>) -> Result<Box<dyn Read>> {
let file = std::io::BufReader::with_capacity(64 * 1024, std::fs::File::open(path)?);
match codec {
Some(algo) => crate::compression::make_reader(algo, file),
None => Ok(Box::new(file)),
}
}
fn ensure_parents(
path: &str,
created: &mut std::collections::HashSet<String>,
sink: &mut dyn RepackSink,
) -> Result<()> {
let trimmed = path.trim_matches('/');
if trimmed.is_empty() {
return Ok(());
}
let parts: Vec<&str> = trimmed.split('/').collect();
let mut cur = String::new();
for seg in &parts[..parts.len() - 1] {
cur.push('/');
cur.push_str(seg);
if created.insert(cur.clone()) {
sink.put_dir(&cur, RepackMeta::dir_default(), &[])?;
}
}
Ok(())
}
pub fn walk_tar_stream(reader: &mut dyn Read, sink: &mut dyn RepackSink) -> Result<()> {
use crate::fs::tar::EntryKind as TarKind;
use crate::fs::tar::stream::TarStreamReader;
fn collapse(p: &str) -> String {
let mut out = String::new();
for seg in p.split('/').filter(|s| !s.is_empty() && *s != ".") {
out.push('/');
out.push_str(seg);
}
if out.is_empty() { "/".to_string() } else { out }
}
let mut tsr = TarStreamReader::new(reader);
let mut created: std::collections::HashSet<String> =
std::collections::HashSet::from(["/".to_string()]);
while let Some(mut se) = tsr.next_entry()? {
let path = collapse(&se.entry.path);
let kind = se.entry.kind;
let size = se.entry.size;
let link = se.entry.link_target.clone();
let (dmaj, dmin) = (se.entry.device_major, se.entry.device_minor);
let meta = RepackMeta {
mode: se.entry.mode,
uid: se.entry.uid,
gid: se.entry.gid,
mtime: se.entry.mtime as u32,
atime: se.entry.mtime as u32,
ctime: se.entry.mtime as u32,
};
let xattrs: Vec<XattrPair> = se
.entry
.xattrs
.iter()
.map(|x| XattrPair {
name: x.name.clone(),
value: x.value.clone(),
})
.collect();
if path == "/" {
continue;
}
note(&path);
ensure_parents(&path, &mut created, sink)?;
match kind {
TarKind::Dir => {
sink.put_dir(&path, meta, &xattrs)?;
created.insert(path);
}
TarKind::Regular => {
sink.put_file(&path, &mut se, size, meta, &xattrs)?;
note_bytes(size);
}
TarKind::Symlink => {
let target = link.as_deref().unwrap_or("");
sink.put_symlink(&path, target, meta, &xattrs)?;
}
TarKind::HardLink => {
let raw = link.as_deref().unwrap_or("");
let target = collapse(&crate::fs::tar::normalise_path(raw));
if !sink.put_hardlink(&path, &target, meta, &xattrs)? {
sink.materialise_copy(&path, &target, meta, &xattrs)?;
}
}
TarKind::CharDev => {
sink.put_device(&path, DeviceKind::Char, dmaj, dmin, meta, &xattrs)?;
}
TarKind::BlockDev => {
sink.put_device(&path, DeviceKind::Block, dmaj, dmin, meta, &xattrs)?;
}
TarKind::Fifo => {
sink.put_device(&path, DeviceKind::Fifo, 0, 0, meta, &xattrs)?;
}
}
}
Ok(())
}
fn walk_host_dir(root: &Path, sink: &mut dyn RepackSink) -> Result<()> {
#[cfg(unix)]
let mut link_map: std::collections::HashMap<u64, String> = std::collections::HashMap::new();
let mut stack: Vec<(PathBuf, String)> = vec![(root.to_path_buf(), "/".to_string())];
while let Some((dir, fs_dir)) = stack.pop() {
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let name = entry.file_name();
let name_str = name.to_str().ok_or_else(|| {
crate::Error::InvalidArgument(format!("repack: non-UTF-8 host filename {name:?}"))
})?;
let dest = join_fs_path(&fs_dir, name_str);
note(&dest);
let meta = entry.metadata()?;
let ft = meta.file_type();
let rmeta = host_meta_to_repack(&meta);
if ft.is_dir() {
sink.put_dir(&dest, rmeta, &[])?;
stack.push((entry.path(), dest));
} else if ft.is_symlink() {
let target = std::fs::read_link(entry.path())?;
let t = target.to_str().ok_or_else(|| {
crate::Error::InvalidArgument("repack: non-UTF-8 symlink target".into())
})?;
sink.put_symlink(&dest, t, rmeta, &[])?;
} else if ft.is_file() {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if meta.nlink() > 1 {
let ino = meta.ino();
if let Some(first) = link_map.get(&ino) {
if sink.put_hardlink(&dest, first, rmeta, &[])? {
continue;
}
} else {
link_map.insert(ino, dest.clone());
}
}
}
let mut f = std::fs::File::open(entry.path())?;
sink.put_file(&dest, &mut f, meta.len(), rmeta, &[])?;
note_bytes(meta.len());
} else {
#[cfg(unix)]
{
use std::os::unix::fs::{FileTypeExt, MetadataExt};
let (kind, major, minor) = if ft.is_char_device() {
let (maj, min) = split_rdev(meta.rdev() as u32);
(Some(DeviceKind::Char), maj, min)
} else if ft.is_block_device() {
let (maj, min) = split_rdev(meta.rdev() as u32);
(Some(DeviceKind::Block), maj, min)
} else if ft.is_fifo() {
(Some(DeviceKind::Fifo), 0, 0)
} else if ft.is_socket() {
(Some(DeviceKind::Socket), 0, 0)
} else {
(None, 0, 0)
};
if let Some(k) = kind {
sink.put_device(&dest, k, major, minor, rmeta, &[])?;
continue;
}
}
eprintln!("repack: skipping unsupported host entry {dest:?}");
}
}
}
Ok(())
}
fn host_meta_to_repack(meta: &std::fs::Metadata) -> RepackMeta {
let fm = host_meta_to_fs(meta);
RepackMeta {
mode: fm.mode,
uid: fm.uid,
gid: fm.gid,
mtime: fm.mtime,
atime: fm.atime,
ctime: fm.ctime,
}
}
pub fn populate_ext_from_source(
dst_dev: &mut dyn crate::block::BlockDevice,
dst: &mut Ext,
source: &Source,
) -> Result<()> {
let mut sink = FsSink::new(dst, dst_dev);
walk_source_into_sink(source, &mut sink)
}
pub fn populate_fat32_from_source(
dst_dev: &mut dyn crate::block::BlockDevice,
dst: &mut crate::fs::fat::Fat32,
source: &Source,
) -> Result<()> {
let mut sink = FsSink::new(dst, dst_dev).lossy();
walk_source_into_sink(source, &mut sink)
}
pub fn ext_build_plan_for_source(
source: &Source,
block_size: u32,
kind: FsKind,
) -> Result<crate::fs::ext::BuildPlan> {
let mut plan = crate::fs::ext::BuildPlan::new(block_size, kind);
match source {
Source::HostDir(p) => plan.scan_host_path(p)?,
Source::TarArchive {
path,
codec: Some(algo),
} => {
let spec = path.to_string_lossy().into_owned();
let index = build_tar_stream_index(&spec, *algo)?;
walk_tar_index_for_plan(&index, &mut plan);
}
Source::TarArchive { path, codec: None } => {
let target = crate::inspect::Target::parse(&path.to_string_lossy());
crate::inspect::with_target_device(&target, |src_dev| {
let mut src_fs = crate::inspect::AnyFs::open(src_dev)?;
build_ext_plan_inner(src_dev, &mut src_fs, &mut plan)
})?;
}
Source::Image(target) => {
crate::inspect::with_target_device(target, |src_dev| {
let mut src_fs = crate::inspect::AnyFs::open(src_dev)?;
build_ext_plan_inner(src_dev, &mut src_fs, &mut plan)
})?;
}
Source::Layered(layers) => {
let model = crate::merge::MergeModel::build(layers)?;
plan = model.analysis(block_size).plan;
plan.kind = kind;
}
}
Ok(plan)
}
pub fn populate_fs_from_source<F: crate::fs::Filesystem>(
dst_dev: &mut dyn crate::block::BlockDevice,
dst: &mut F,
source: &Source,
) -> Result<()> {
populate_fs_from_source_dyn(dst_dev, dst, source)
}
pub fn populate_fs_from_source_dyn(
dst_dev: &mut dyn crate::block::BlockDevice,
dst: &mut dyn crate::fs::Filesystem,
source: &Source,
) -> Result<()> {
let mut sink = FsSink::new(dst, dst_dev).lossy();
walk_source_into_sink(source, &mut sink)
}
fn host_meta_to_fs(meta: &std::fs::Metadata) -> crate::fs::FileMeta {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(unix)]
{
crate::fs::FileMeta {
mode: (meta.mode() & 0o7777) as u16,
uid: meta.uid(),
gid: meta.gid(),
mtime: meta.mtime() as u32,
atime: meta.atime() as u32,
ctime: meta.ctime() as u32,
}
}
#[cfg(not(unix))]
{
let _ = meta;
crate::fs::FileMeta::default()
}
}
pub fn fat32_min_bytes_for_source(source: &Source) -> Result<u64> {
let bytes = match source {
Source::HostDir(p) => sum_host_dir_bytes(p)?,
Source::TarArchive {
path,
codec: Some(algo),
} => {
let spec = path.to_string_lossy().into_owned();
let index = build_tar_stream_index(&spec, *algo)?;
let (sz, _, _, _, _, _) = size_from_tar_index(&index, "fat32")?;
return Ok(sz);
}
Source::TarArchive { path, codec: None } => {
let target = crate::inspect::Target::parse(&path.to_string_lossy());
let mut sum = 0u64;
crate::inspect::with_target_device(&target, |src_dev| {
let mut src_fs = crate::inspect::AnyFs::open(src_dev)?;
sum = sum_source_file_bytes(src_dev, &mut src_fs)?;
Ok(())
})?;
sum
}
Source::Image(target) => {
let mut sum = 0u64;
crate::inspect::with_target_device(target, |src_dev| {
let mut src_fs = crate::inspect::AnyFs::open(src_dev)?;
sum = sum_source_file_bytes(src_dev, &mut src_fs)?;
Ok(())
})?;
sum
}
Source::Layered(layers) => {
let model = crate::merge::MergeModel::build(layers)?;
model.analysis(1024).total_file_bytes
}
};
let needed = bytes
.saturating_mul(2)
.max(crate::fs::fat::MIN_FAT32_CLUSTERS as u64 * 1024);
Ok(needed.div_ceil(512) * 512)
}
fn sum_host_dir_bytes(root: &Path) -> Result<u64> {
let mut total = 0u64;
let mut stack: Vec<PathBuf> = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let meta = entry.metadata()?;
if meta.is_dir() {
stack.push(entry.path());
} else if meta.is_file() {
total = total.saturating_add(meta.len());
}
}
}
Ok(total)
}
fn build_ext_plan_inner(
src_dev: &mut dyn crate::block::BlockDevice,
src_fs: &mut crate::inspect::AnyFs,
plan: &mut crate::fs::ext::BuildPlan,
) -> Result<()> {
build_ext_plan_through_trait(src_dev, src_fs, plan)
}
pub fn build_ext_plan_through_trait(
src_dev: &mut dyn crate::block::BlockDevice,
src_fs: &mut crate::inspect::AnyFs,
plan: &mut crate::fs::ext::BuildPlan,
) -> Result<()> {
src_fs.as_filesystem_dyn(|fs| scan_into_build_plan(src_dev, fs, plan))
}
pub(crate) fn scan_into_build_plan(
dev: &mut dyn crate::block::BlockDevice,
fs: &mut dyn crate::fs::Filesystem,
plan: &mut crate::fs::ext::BuildPlan,
) -> Result<()> {
use crate::fs::EntryKind;
let mut stack: Vec<std::path::PathBuf> = vec![std::path::PathBuf::from("/")];
while let Some(dir) = stack.pop() {
let entries = fs.list(dev, &dir)?;
for e in entries {
if e.name == "." || e.name == ".." || e.name == "lost+found" {
continue;
}
let child = dir.join(&e.name);
match e.kind {
EntryKind::Dir => {
plan.add_dir();
stack.push(child);
}
EntryKind::Regular => plan.add_file(e.size),
EntryKind::Symlink => {
let len = fs
.read_symlink(dev, &child)
.map(|t| t.as_os_str().len())
.unwrap_or(usize::MAX);
plan.add_symlink(len);
}
EntryKind::Char | EntryKind::Block | EntryKind::Fifo | EntryKind::Socket => {
plan.add_device()
}
EntryKind::Unknown => {}
}
}
}
Ok(())
}
fn walk_tar_index_for_plan(
index: &crate::fs::tar::TarStreamIndex,
plan: &mut crate::fs::ext::BuildPlan,
) {
use crate::fs::tar::EntryKind as TarKind;
for ix in index.entries() {
match ix.entry.kind {
TarKind::Regular | TarKind::HardLink => plan.add_file(ix.entry.size),
TarKind::Dir => plan.add_dir(),
TarKind::Symlink => plan.add_symlink(
ix.entry
.link_target
.as_deref()
.map(|s| s.len())
.unwrap_or(0),
),
TarKind::CharDev | TarKind::BlockDev | TarKind::Fifo => plan.add_device(),
}
}
}
pub(crate) fn tar_output_codec(path: &std::path::Path) -> Option<crate::compression::Algo> {
let s = path.to_string_lossy().to_ascii_lowercase();
if s.ends_with(".tgz") {
return Some(crate::compression::Algo::Gzip);
}
if s.ends_with(".txz") {
return Some(crate::compression::Algo::Xz);
}
if !s.contains(".tar.") {
return None;
}
crate::compression::Algo::from_extension(path)
}
pub(crate) fn tar_input_codec(path: &str) -> Option<crate::compression::Algo> {
let p = std::path::Path::new(path.split(':').next().unwrap_or(path));
tar_output_codec(p)
}
pub(crate) fn build_tar_stream_index(
src: &str,
algo: crate::compression::Algo,
) -> crate::Result<crate::fs::tar::TarStreamIndex> {
let reader = open_tar_stream_reader(src, Some(algo))?;
crate::fs::tar::TarStreamIndex::build_from(reader)
}
pub(crate) fn size_from_tar_index(
index: &crate::fs::tar::TarStreamIndex,
target_lower: &str,
) -> crate::Result<(u64, u64, u64, u64, u64, u64)> {
use crate::fs::tar::EntryKind as TarKind;
let mut files = 0u64;
let mut dirs = 0u64;
let mut symlinks = 0u64;
let mut devices = 0u64;
let mut bytes = 0u64;
for ix in index.entries() {
match ix.entry.kind {
TarKind::Regular => {
files += 1;
bytes += ix.entry.size;
}
TarKind::HardLink => {
files += 1;
bytes += ix.entry.size;
}
TarKind::Dir => dirs += 1,
TarKind::Symlink => symlinks += 1,
TarKind::CharDev | TarKind::BlockDev | TarKind::Fifo => devices += 1,
}
}
let size_estimate = match target_lower {
"ext2" | "ext3" | "ext4" => {
let inodes = files + dirs + symlinks + devices + 16;
let raw = bytes + inodes * 4096 + 1024 * 1024;
raw.max(8 * 1024 * 1024).div_ceil(4096) * 4096
}
"fat32" | "vfat" => {
let needed = bytes
.saturating_mul(2)
.max(crate::fs::fat::MIN_FAT32_CLUSTERS as u64 * 1024);
needed.div_ceil(512) * 512
}
_ => bytes + 16 * 1024 * 1024,
};
Ok((size_estimate, files, dirs, symlinks, devices, bytes))
}
pub(crate) fn open_tar_stream_reader(
path: &str,
algo: Option<crate::compression::Algo>,
) -> crate::Result<crate::fs::tar::TarStreamReader<Box<dyn std::io::Read>>> {
let p = match path.rsplit_once(':') {
Some((head, tail)) if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) => {
std::path::Path::new(head)
}
_ => std::path::Path::new(path),
};
let file = std::fs::File::open(p)?;
let buffered: Box<dyn std::io::Read> =
Box::new(std::io::BufReader::with_capacity(64 * 1024, file));
let inner: Box<dyn std::io::Read> = match algo {
Some(a) => crate::compression::make_reader(a, buffered)?,
None => buffered,
};
Ok(crate::fs::tar::TarStreamReader::new(inner))
}
pub fn open_tar_stream_index(
image: &str,
algo: Option<crate::compression::Algo>,
) -> crate::Result<crate::fs::tar::TarStreamIndex> {
let reader = open_tar_stream_reader(image, algo)?;
crate::fs::tar::TarStreamIndex::build_from(reader)
}
pub(crate) fn join_fs_path(parent: &str, leaf: &str) -> String {
if parent.ends_with('/') {
format!("{parent}{leaf}")
} else {
format!("{parent}/{leaf}")
}
}
pub(crate) fn sum_source_file_bytes(
src_dev: &mut dyn crate::block::BlockDevice,
src_fs: &mut crate::inspect::AnyFs,
) -> crate::Result<u64> {
src_fs.total_file_bytes(src_dev)
}
#[cfg(test)]
mod progress_tests {
use super::truncate_left;
#[test]
fn truncate_left_fits_returns_full() {
assert_eq!(truncate_left("short.txt", 10), "short.txt");
assert_eq!(truncate_left("9chars.tx", 9), "9chars.tx");
}
#[test]
fn truncate_left_keeps_trailing_filename() {
let p = "/var/log/path/to/deep/file.log";
let out = truncate_left(p, 10);
assert_eq!(out, "…/file.log");
assert!(out.chars().count() <= 10);
}
#[test]
fn truncate_left_zero_budget_empty() {
assert_eq!(truncate_left("x", 0), "");
}
#[test]
fn truncate_left_unicode_path() {
let p = "/données/important/документ.txt";
let out = truncate_left(p, 15);
assert!(out.starts_with('…'));
assert!(out.chars().count() <= 15);
assert!(out.contains(".txt"));
}
}
#[cfg(test)]
mod ticker_layout_tests {
use super::Progress;
fn make() -> Progress {
let mut p = Progress::auto();
p.is_tty = true;
p.files = 42;
p.last_path = "/very/deep/nested/dir/some-long-filename.bin".into();
p
}
#[test]
fn ticker_no_cols_keeps_full_path() {
let p = make();
let line = p.status_line(None);
assert!(line.contains("/very/deep/nested/dir/some-long-filename.bin"));
}
#[test]
fn ticker_narrow_pty_truncates_left() {
let p = make();
let line = p.status_line(Some(60));
assert!(8 + line.chars().count() <= 60);
assert!(line.contains("filename.bin"));
assert!(line.contains('…'));
}
#[test]
fn ticker_tight_pty_still_shows_basename_suffix() {
let p = make();
let line = p.status_line(Some(30));
assert!(8 + line.chars().count() <= 30);
assert!(line.contains(".bin"));
}
}