use std::collections::BTreeMap;
use std::io::Read;
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::ext::{BuildPlan, FsKind};
use crate::fs::{DeviceKind, XattrPair};
use crate::inspect::AnyFs;
use crate::repack::{RepackMeta, RepackSink, Source, walk_anyfs, walk_source_into_sink};
pub const SIZED_FS_TYPES: &[&str] = &["ext2", "ext3", "ext4", "fat32"];
#[derive(Debug, Clone)]
pub struct Analysis {
pub files: u64,
pub dirs: u64,
pub symlinks: u64,
pub devices: u64,
pub hardlinks: u64,
pub total_file_bytes: u64,
pub(crate) plan: BuildPlan,
}
impl Analysis {
pub fn inode_count(&self) -> u32 {
self.plan.inodes_count()
}
pub fn block_size(&self) -> u32 {
self.plan.block_size
}
pub fn recommended_size(&self, fs_type: &str) -> Option<u64> {
let lower = fs_type.to_ascii_lowercase();
match lower.as_str() {
"ext2" | "ext3" | "ext4" => {
let mut p = self.plan.clone();
p.kind = match lower.as_str() {
"ext2" => FsKind::Ext2,
"ext3" => FsKind::Ext3,
_ => FsKind::Ext4,
};
Some(p.blocks_count() as u64 * p.block_size as u64)
}
"fat32" | "vfat" => {
let needed = self
.total_file_bytes
.saturating_mul(2)
.max(crate::fs::fat::MIN_FAT32_CLUSTERS as u64 * 1024);
Some(needed.div_ceil(512) * 512)
}
_ => None,
}
}
pub fn ext_format_opts(&self, kind: FsKind) -> crate::fs::ext::FormatOpts {
let mut p = self.plan.clone();
p.kind = kind;
p.to_format_opts()
}
pub fn report(&self, fs_types: &[&str]) -> AnalysisReport {
let mut recommended_size = BTreeMap::new();
for &t in fs_types {
if let Some(sz) = self.recommended_size(t) {
recommended_size.insert(t.to_string(), sz);
}
}
AnalysisReport {
files: self.files,
dirs: self.dirs,
symlinks: self.symlinks,
devices: self.devices,
hardlinks: self.hardlinks,
total_file_bytes: self.total_file_bytes,
inode_count: self.inode_count(),
block_size: self.block_size(),
recommended_size,
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct AnalysisReport {
pub files: u64,
pub dirs: u64,
pub symlinks: u64,
pub devices: u64,
pub hardlinks: u64,
pub total_file_bytes: u64,
pub inode_count: u32,
pub block_size: u32,
pub recommended_size: BTreeMap<String, u64>,
}
struct AnalysisSink {
a: Analysis,
}
impl AnalysisSink {
fn new(block_size: u32) -> Self {
Self {
a: Analysis {
files: 0,
dirs: 0,
symlinks: 0,
devices: 0,
hardlinks: 0,
total_file_bytes: 0,
plan: BuildPlan::new(block_size, FsKind::Ext4),
},
}
}
}
impl RepackSink for AnalysisSink {
fn put_dir(&mut self, _path: &str, _meta: RepackMeta, _xattrs: &[XattrPair]) -> Result<()> {
self.a.dirs += 1;
self.a.plan.add_dir();
Ok(())
}
fn put_file(
&mut self,
_path: &str,
_body: &mut dyn Read,
len: u64,
_meta: RepackMeta,
_xattrs: &[XattrPair],
) -> Result<()> {
self.a.files += 1;
self.a.total_file_bytes = self.a.total_file_bytes.saturating_add(len);
self.a.plan.add_file(len);
Ok(())
}
fn put_symlink(
&mut self,
_path: &str,
target: &str,
_meta: RepackMeta,
_xattrs: &[XattrPair],
) -> Result<()> {
self.a.symlinks += 1;
self.a.plan.add_symlink(target.len());
Ok(())
}
fn put_device(
&mut self,
_path: &str,
_kind: DeviceKind,
_major: u32,
_minor: u32,
_meta: RepackMeta,
_xattrs: &[XattrPair],
) -> Result<()> {
self.a.devices += 1;
self.a.plan.add_device();
Ok(())
}
fn put_hardlink(
&mut self,
_path: &str,
_target: &str,
_meta: RepackMeta,
_xattrs: &[XattrPair],
) -> Result<bool> {
self.a.hardlinks += 1;
self.a.plan.add_file(0);
Ok(true)
}
fn finish(&mut self) -> Result<()> {
Ok(())
}
}
pub fn analyze_source(source: &Source, block_size: u32) -> Result<Analysis> {
let mut sink = AnalysisSink::new(block_size);
walk_source_into_sink(source, &mut sink)?;
Ok(sink.a)
}
pub fn analyze_fs(fs: &mut AnyFs, dev: &mut dyn BlockDevice, block_size: u32) -> Result<Analysis> {
let mut sink = AnalysisSink::new(block_size);
walk_anyfs(fs, dev, &mut sink)?;
Ok(sink.a)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repack::RepackMeta;
fn meta() -> RepackMeta {
RepackMeta {
mode: 0o644,
uid: 0,
gid: 0,
mtime: 0,
atime: 0,
ctime: 0,
}
}
#[test]
fn sink_accumulates_counts_and_sizes() {
let mut sink = AnalysisSink::new(4096);
sink.put_dir("/d", meta(), &[]).unwrap();
sink.put_file("/d/a", &mut std::io::empty(), 4096, meta(), &[])
.unwrap();
sink.put_file("/d/b", &mut std::io::empty(), 100, meta(), &[])
.unwrap();
sink.put_symlink("/d/l", "a", meta(), &[]).unwrap();
sink.put_device("/d/dev", DeviceKind::Char, 1, 3, meta(), &[])
.unwrap();
sink.put_hardlink("/d/h", "/d/a", meta(), &[]).unwrap();
let a = sink.a;
assert_eq!(a.files, 2);
assert_eq!(a.dirs, 1);
assert_eq!(a.symlinks, 1);
assert_eq!(a.devices, 1);
assert_eq!(a.hardlinks, 1);
assert_eq!(a.total_file_bytes, 4196);
assert_eq!(a.inode_count(), 24);
let mut p = a.plan.clone();
p.kind = FsKind::Ext4;
assert_eq!(
a.recommended_size("ext4"),
Some(p.blocks_count() as u64 * 4096)
);
let want_fat = (a.total_file_bytes * 2)
.max(crate::fs::fat::MIN_FAT32_CLUSTERS as u64 * 1024)
.div_ceil(512)
* 512;
assert_eq!(a.recommended_size("fat32"), Some(want_fat));
}
#[test]
fn recommended_size_none_for_streamed_outputs() {
let a = AnalysisSink::new(1024).a;
for t in ["tar", "zip", "cpio", "ar", "grf", "iso", "iso9660"] {
assert_eq!(a.recommended_size(t), None, "{t} should be None");
}
for t in ["ext2", "ext3", "ext4", "fat32", "vfat"] {
assert!(a.recommended_size(t).is_some(), "{t} should be Some");
}
assert!(a.recommended_size("ext4").unwrap() > a.recommended_size("ext2").unwrap());
}
}