use std::ffi::OsString;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command as Cmd};
use fs_core::cli::{CliError, Json, Outcome, Tool};
use fs_ext4::fsck::{audit_with_repair, Anomaly, AuditReport};
pub const CLEAN: u8 = 0;
pub const CORRECTED: u8 = 1;
pub const UNCORRECTED: u8 = 4;
pub const OPERATIONAL: u8 = 8;
pub const USAGE: u8 = 16;
pub const TOOL: Tool = Tool {
name: "fsck.ext4",
verb: "fsck",
section: 8,
usage_exit: USAGE,
about: "Check an ext4 filesystem, and repair what can be repaired",
command,
run,
};
fn command() -> Cmd {
Cmd::new("fsck.ext4")
.about("Check an ext4 filesystem, and repair what can be repaired")
.long_about(
"Check an ext4 image or device: the directory tree, link counts, `..` \
entries, entry types, directory-block checksums, and the free counts in the \
group descriptors and the superblock.\n\n\
Nothing is written unless -y (or -p) is given. The report is JSON on stdout \
(--text for people).\n\n\
Exit status, as fsck(8): 0 clean, 1 errors corrected, 4 errors left \
uncorrected, 8 operational error, 16 usage error.",
)
.arg(
Arg::new("device")
.value_name("TARGET")
.help("The image file or device to check")
.value_parser(value_parser!(OsString))
.required(true),
)
.arg(
Arg::new("no")
.short('n')
.long("no")
.help("Check only; open read-only and change nothing (the default)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("yes")
.short('y')
.long("yes")
.help("Repair everything that can be repaired")
.action(ArgAction::SetTrue)
.conflicts_with("no"),
)
.arg(
Arg::new("preen")
.short('p')
.visible_short_alias('a')
.long("preen")
.help("Repair automatically, as -y (every repair made here is a safe one)")
.action(ArgAction::SetTrue)
.conflicts_with("no"),
)
.arg(
Arg::new("force")
.short('f')
.long("force")
.help("Check even a volume marked clean (accepted: every check is full)")
.action(ArgAction::Count),
)
.arg(
Arg::new("verbose")
.short('v')
.help("Accepted for compatibility; the report already lists every finding")
.action(ArgAction::Count),
)
.arg(
Arg::new("offset")
.long("offset")
.value_name("BYTES")
.help(
"Where the filesystem starts in TARGET, for a partition in a whole-disk image",
)
.value_parser(value_parser!(u64)),
)
.args(fs_core::cli::format_args())
.after_help(
"Examples:\n fsck.ext4 disk.img check, change nothing\n \
fsck.ext4 -fn disk.img the same, as e2fsck spells it\n \
fsck.ext4 -y disk.img repair what can be repaired\n \
fsck.ext4 --text disk.img; echo $?",
)
}
fn run(matches: &ArgMatches) -> Result<Outcome, CliError> {
let target = matches
.get_one::<OsString>("device")
.expect("clap requires the device");
let offset = matches.get_one::<u64>("offset").copied().unwrap_or(0);
let repair = matches.get_flag("yes") || matches.get_flag("preen");
let name = target.to_string_lossy().into_owned();
let fs = super::device::mount(target, offset, repair).map_err(|e| e.with_code(OPERATIONAL))?;
let dirty = super::fs::is_dirty(&fs);
let mut findings = Vec::new();
let mut refused = None;
let mut result = audit_with_repair(
&fs,
u32::MAX,
u32::MAX,
|_, _, _| {},
|a| findings.push(finding(a)),
repair,
);
if repair {
if let Err(e @ (fs_ext4::Error::Unsupported(_) | fs_ext4::Error::ReadOnly)) = &result {
refused = Some(format!("repair refused: {e}"));
findings.clear();
result = audit_with_repair(
&fs,
u32::MAX,
u32::MAX,
|_, _, _| {},
|a| findings.push(finding(a)),
false,
);
}
}
let mode = if repair && refused.is_none() {
"repair"
} else {
"check"
};
let report = match result {
Ok(report) => report,
Err(e @ fs_ext4::Error::Io(_)) => {
return Err(CliError::failed(format!("{name}: {e}")).with_code(OPERATIONAL));
}
Err(e) => {
findings.push(Json::object([
("kind", Json::from("unreadable")),
("detail", Json::from(e.to_string())),
]));
let json = Json::object([
("fs", Json::from("ext4")),
("device", Json::from(name.as_str())),
("mode", Json::from(mode)),
("clean", Json::from(false)),
("dirty", Json::from(dirty)),
("exit", Json::from(u64::from(UNCORRECTED))),
("error", Json::from(format!("the check stopped: {e}"))),
("findings", Json::Arr(findings)),
]);
let text = format!("fsck.ext4: {name}: the check stopped: {e}");
return Ok(Outcome::report(json).with_text(text).with_code(UNCORRECTED));
}
};
if repair && refused.is_none() {
fs.finish().map_err(|e| {
CliError::failed(format!("{name}: finishing the repair: {e}")).with_code(OPERATIONAL)
})?;
}
let code = exit_code(&report);
let mut fields = vec![
("fs", Json::from("ext4")),
("device", Json::from(name.as_str())),
("mode", Json::from(mode)),
("clean", Json::from(report.anomalies_count == 0)),
("dirty", Json::from(dirty)),
("exit", Json::from(u64::from(code))),
("found", Json::from(report.initial_anomalies_count)),
("repaired", Json::from(report.repaired_count)),
("remaining", Json::from(report.anomalies_count)),
(
"scanned",
Json::object([
("directories", Json::from(report.directories_scanned)),
("entries", Json::from(report.entries_scanned)),
("inodes", Json::from(report.inodes_visited)),
]),
),
("findings", Json::Arr(findings.clone())),
];
if let Some(why) = &refused {
fields.push(("refused", Json::from(why.as_str())));
}
let mut text = vec![match code {
CLEAN => format!(
"fsck.ext4: {name}: clean ({} directories, {} entries)",
report.directories_scanned, report.entries_scanned
),
CORRECTED => format!(
"fsck.ext4: {name}: {} problems found, all corrected",
report.initial_anomalies_count
),
_ => format!(
"fsck.ext4: {name}: {} problems found, {} repaired, {} remaining",
report.initial_anomalies_count, report.repaired_count, report.anomalies_count
),
}];
if let Some(why) = refused {
text.push(format!(" {why}"));
}
for f in &findings {
text.push(format!(" {}", f.to_text().replace('\n', ", ")));
}
Ok(Outcome::report(Json::object(fields))
.with_text(text.join("\n"))
.with_code(code))
}
fn exit_code(report: &AuditReport) -> u8 {
if report.anomalies_count > 0 {
UNCORRECTED
} else if report.repaired_count > 0 || report.initial_anomalies_count > 0 {
CORRECTED
} else {
CLEAN
}
}
fn finding(a: &Anomaly) -> Json {
let pairs: Vec<(&str, Json)> = match a {
Anomaly::LinkCountTooLow {
ino,
stored,
observed,
} => vec![
("kind", "link_count_too_low".into()),
("inode", (*ino).into()),
("stored", (*stored).into()),
("observed", (*observed).into()),
],
Anomaly::LinkCountTooHigh {
ino,
stored,
observed,
} => vec![
("kind", "link_count_too_high".into()),
("inode", (*ino).into()),
("stored", (*stored).into()),
("observed", (*observed).into()),
],
Anomaly::DanglingEntry {
parent_ino,
child_ino,
observed,
} => vec![
("kind", "dangling_entry".into()),
("parent", (*parent_ino).into()),
("inode", (*child_ino).into()),
("observed", (*observed).into()),
],
Anomaly::WrongDotDot {
dir_ino,
claims,
actual_parent,
} => vec![
("kind", "wrong_dotdot".into()),
("inode", (*dir_ino).into()),
("claims", (*claims).into()),
("parent", (*actual_parent).into()),
],
Anomaly::BogusEntry {
parent_ino,
child_ino,
name,
} => vec![
("kind", "bogus_entry".into()),
("parent", (*parent_ino).into()),
("inode", (*child_ino).into()),
("name", String::from_utf8_lossy(name).into_owned().into()),
],
Anomaly::BlockGroupFreeCountDrift {
group_index,
stored_blocks,
observed_blocks,
stored_inodes,
observed_inodes,
} => vec![
("kind", "group_free_count_drift".into()),
("group", (*group_index).into()),
("stored_blocks", (*stored_blocks).into()),
("observed_blocks", (*observed_blocks).into()),
("stored_inodes", (*stored_inodes).into()),
("observed_inodes", (*observed_inodes).into()),
],
Anomaly::SuperblockFreeCountDrift {
stored_blocks,
observed_blocks,
stored_inodes,
observed_inodes,
} => vec![
("kind", "superblock_free_count_drift".into()),
("stored_blocks", (*stored_blocks).into()),
("observed_blocks", (*observed_blocks).into()),
("stored_inodes", (*stored_inodes).into()),
("observed_inodes", (*observed_inodes).into()),
],
Anomaly::DuplicateDirentForDirInode { ino, dirents } => vec![
("kind", "duplicate_directory_entry".into()),
("inode", (*ino).into()),
(
"entries",
Json::Arr(
dirents
.iter()
.map(|(parent, name)| {
Json::object([
("parent", Json::from(*parent)),
("name", Json::from(name.as_str())),
])
})
.collect(),
),
),
],
Anomaly::DirBlockChecksumMismatch {
dir_ino,
logical_block,
htree,
} => vec![
("kind", "directory_block_checksum".into()),
("inode", (*dir_ino).into()),
("block", (*logical_block).into()),
("htree", (*htree).into()),
],
Anomaly::UnreadableDirectory { ino, reason } => vec![
("kind", "unreadable_directory".into()),
("inode", (*ino).into()),
("reason", Json::from(reason.as_str())),
],
other => vec![
("kind", "other".into()),
("detail", format!("{other:?}").into()),
],
};
Json::object(pairs)
}