#![forbid(unsafe_code)]
pub mod graph;
pub mod repair;
pub mod scan;
pub mod verify;
use std::collections::HashMap;
use std::path::Path;
use crate::core::limits::Limits;
use crate::format::version::RecordTag;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
Superblock,
Segment,
Record,
Root,
Inode,
Directory,
Extent,
ChunkIndex,
Snapshot,
Reference,
Reachability,
Graph,
Repair,
}
impl Category {
pub const fn name(self) -> &'static str {
match self {
Category::Superblock => "superblock",
Category::Segment => "segment",
Category::Record => "record",
Category::Root => "root",
Category::Inode => "inode",
Category::Directory => "directory",
Category::Extent => "extent",
Category::ChunkIndex => "chunk-index",
Category::Snapshot => "snapshot",
Category::Reference => "reference",
Category::Reachability => "reachability",
Category::Graph => "graph",
Category::Repair => "repair",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FsckIssue {
pub severity: Severity,
pub category: Category,
pub message: String,
}
impl FsckIssue {
pub fn new(severity: Severity, category: Category, message: impl Into<String>) -> Self {
Self {
severity,
category,
message: message.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct FsckOptions {
pub verify_materialized: bool,
pub max_records_per_segment: u64,
pub max_fanout: u32,
pub max_descriptor_bytes: u64,
pub max_inline_bytes: u64,
pub max_palette: usize,
pub max_period: u32,
pub max_chunk_size: u64,
pub repair_torn_tails: bool,
}
impl Default for FsckOptions {
fn default() -> Self {
let l = Limits::default();
Self {
verify_materialized: false,
max_records_per_segment: 1_000_000,
max_fanout: l.max_fanout,
max_descriptor_bytes: l.max_descriptor_bytes,
max_inline_bytes: l.max_inline_bytes,
max_palette: l.max_palette,
max_period: l.max_period,
max_chunk_size: l.max_chunk_size,
repair_torn_tails: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FsckReport {
pub superblock_slots_valid: u8,
pub segments_scanned: u64,
pub records_scanned: u64,
pub records_by_tag: HashMap<RecordTag, u64>,
pub live_objects: u64,
pub leaked_objects: u64,
pub leaked_bytes: u64,
pub inodes_verified: u64,
pub extents_verified: u64,
pub chunk_descriptors_verified: u64,
pub issues: Vec<FsckIssue>,
pub repaired: Vec<String>,
}
impl FsckReport {
pub fn is_clean(&self) -> bool {
!self.issues.iter().any(|i| i.severity == Severity::Error)
}
pub fn error_count(&self) -> usize {
self.issues
.iter()
.filter(|i| i.severity == Severity::Error)
.count()
}
pub fn warning_count(&self) -> usize {
self.issues
.iter()
.filter(|i| i.severity == Severity::Warning)
.count()
}
pub fn render(&self) -> String {
let mut s = String::new();
s.push_str(&format!(
"superblock slots valid: {}/{}\n",
self.superblock_slots_valid, 2
));
s.push_str(&format!("segments scanned: {}\n", self.segments_scanned));
s.push_str(&format!("records scanned: {}\n", self.records_scanned));
s.push_str(&format!("live objects: {}\n", self.live_objects));
s.push_str(&format!(
"leaked objects: {} ({} bytes)\n",
self.leaked_objects, self.leaked_bytes
));
s.push_str(&format!("inodes verified: {}\n", self.inodes_verified));
s.push_str(&format!("extents verified: {}\n", self.extents_verified));
s.push_str(&format!(
"chunk descriptors verified: {}\n",
self.chunk_descriptors_verified
));
if !self.repaired.is_empty() {
for r in &self.repaired {
s.push_str(&format!("repaired: {r}\n"));
}
}
for issue in &self.issues {
s.push_str(&format!(
"{:>7} [{}] {}\n",
format!("{:?}", issue.severity).to_lowercase(),
issue.category.name(),
issue.message
));
}
if self.issues.is_empty() {
s.push_str("no issues found\n");
}
s
}
}
pub fn fsck(dir: &Path, options: &FsckOptions) -> Result<FsckReport, String> {
let mut ctx = scan::FsckCtx::scan(dir, options)?;
let mut report = FsckReport {
superblock_slots_valid: ctx.slots.len() as u8,
..Default::default()
};
verify::verify_all(&mut ctx)?;
let live = graph::mark_live(&mut ctx)?;
let (leaked_objects, leaked_bytes) = graph::leaked(&ctx, &live);
graph::report_leaks(&mut ctx, &live)?;
report.repaired = repair::repair(&mut ctx)?;
report.segments_scanned = ctx.segments_scanned;
report.records_scanned = ctx.records_scanned;
report.records_by_tag = ctx.records_by_tag;
report.live_objects = live.len() as u64;
report.leaked_objects = leaked_objects;
report.leaked_bytes = leaked_bytes;
report.inodes_verified = ctx.inodes_verified;
report.extents_verified = ctx.extents_verified;
report.chunk_descriptors_verified = ctx.chunk_descriptors_verified;
report.issues = ctx.issues;
Ok(report)
}
pub fn ensure_unmounted(dir: &Path) -> Result<(), String> {
use std::fs::OpenOptions;
let path = dir.join("lock");
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&path)
.map_err(|e| format!("lock open: {e}"))?;
use rustix::fs::{FlockOperation, flock};
match flock(&file, FlockOperation::NonBlockingLockExclusive) {
Ok(()) => Ok(()),
Err(rustix::io::Errno::WOULDBLOCK) => Err(
"store is mounted or otherwise in use (mount lock held); unmount before fsck".into(),
),
Err(e) => Err(format!("lock: {e}")),
}
}