use super::super::error::{BackupDamage, DamageSide, PersistError};
use super::super::manifest::{FIXED_BYTES, ManifestFixed};
use super::super::wal::{JournalReader, JournalRecord, WAL_HEADER_BYTES};
use super::files::{
DirPath, GRAPH_COPY, GRAPH_FILE, GraphFile, MARKS_FILE, WAL_DIR, read_bounded, wal_name,
};
use super::maintenance::VerifyProblem;
use super::snapshot_dir::{MANIFEST_FILE, verify_snapshot_dir};
use super::wal_files::{decode_marks, open_segment_reader};
#[derive(Debug, Default)]
pub(super) struct Findings {
pub(super) problems: Vec<VerifyProblem>,
pub(super) warnings: Vec<String>,
}
impl Findings {
pub(super) fn problem(&mut self, path: impl Into<String>, offset: Option<u64>, reason: String) {
self.problems.push(VerifyProblem {
path: path.into(),
offset,
reason,
});
}
pub(super) fn is_ok(&self) -> bool {
self.problems.is_empty()
}
pub(super) fn into_error(
self,
side: DamageSide,
store: &DirPath,
backup: String,
from_memory: bool,
) -> PersistError {
PersistError::BackupDamaged(Box::new(BackupDamage {
side,
store: store.display(),
backup,
problems: self.problems,
from_memory,
}))
}
}
pub(super) fn check_graph_files(
dir: &DirPath,
findings: &mut Findings,
) -> Result<Option<GraphFile>, PersistError> {
Ok(match super::files::decode_graph_copies(dir)? {
(Ok(main), Ok(copy)) => Some(if copy.lineage.len() >= main.lineage.len() {
copy
} else {
main
}),
(Ok(file), Err(err)) | (Err(err), Ok(file)) => {
findings.warnings.push(format!(
"one copy of GRAPH is not usable ({err}); the other one was used and the backup \
has two intact copies (`graphersal store verify` lists it; a repair or the next \
read-write open of the store rewrites it)"
));
Some(file)
}
(Err(main), Err(copy)) => {
findings.problem(GRAPH_FILE, None, format!("not usable: {main}"));
findings.problem(GRAPH_COPY, None, format!("not usable: {copy}"));
None
}
})
}
pub(super) fn check_marks(dir: &DirPath, findings: &mut Findings) -> Option<Vec<u8>> {
let path = dir.join(MARKS_FILE);
if !path.exists() {
return None;
}
match read_bounded(&path, 256 << 20) {
Ok(bytes) if decode_marks(&bytes).is_some() => Some(bytes),
Ok(_) => {
findings.warnings.push(
"the store's marks file is damaged (it is derived from the WAL): the backup's \
is rebuilt from the WAL it holds"
.into(),
);
None
}
Err(err) => {
findings.warnings.push(format!(
"the store's marks file cannot be read ({err}): the backup's is rebuilt from \
the WAL it holds"
));
None
}
}
}
pub(super) fn check_snapshot(
dir: &DirPath,
snapshot: &DirPath,
heal: bool,
findings: &mut Findings,
) -> Option<Vec<u8>> {
let relative = snapshot.relative_to(dir);
if let Err(err) = verify_snapshot_dir(snapshot) {
let root = dir.display();
match &err {
PersistError::Corrupt {
location,
offset,
reason,
} => {
let file = location
.strip_prefix(&root)
.map(|rest| rest.trim_start_matches('/').to_string())
.unwrap_or_else(|| relative.clone());
findings.problem(file, Some(*offset), reason.clone());
}
other => findings.problem(relative, None, other.to_string()),
}
return None;
}
let path = snapshot.join(MANIFEST_FILE);
let bytes = match read_bounded(&path, 256 << 20) {
Ok(bytes) if bytes.len() >= 2 * FIXED_BYTES => bytes,
Ok(_) => {
findings.problem(
path.relative_to(dir),
None,
"shorter than its two fixed parts".into(),
);
return None;
}
Err(err) => {
findings.problem(path.relative_to(dir), None, err.to_string());
return None;
}
};
let tail_at = bytes.len() - FIXED_BYTES;
let head = ManifestFixed::decode(&bytes[..FIXED_BYTES]);
let tail = ManifestFixed::decode(&bytes[tail_at..]);
let name = path.relative_to(dir);
match (head, tail) {
(Ok(h), Ok(t)) if h == t => None,
(Ok(_), Err(err)) | (Err(err), Ok(_)) if heal => {
let mut healed = bytes.clone();
if ManifestFixed::decode(&bytes[..FIXED_BYTES]).is_ok() {
healed[tail_at..].copy_from_slice(&bytes[..FIXED_BYTES]);
} else {
healed[..FIXED_BYTES].copy_from_slice(&bytes[tail_at..]);
}
findings.warnings.push(format!(
"{name}: one copy of the fixed part is damaged ({}); the intact one was used \
and the backup holds it in both places: plan a repair of the store",
err.reason
));
Some(healed)
}
(Ok(_), Ok(_)) => {
findings.problem(name, None, "the two copies of the fixed part differ".into());
None
}
(Err(err), _) => {
findings.problem(name, Some(err.offset), format!("head copy: {}", err.reason));
None
}
(_, Err(err)) => {
findings.problem(
name,
Some(tail_at as u64 + err.offset),
format!("tail copy: {}", err.reason),
);
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct SegmentCheck {
pub(super) end: u64,
pub(super) last_record: Option<u64>,
pub(super) commits: Vec<(u64, u64)>,
}
pub(super) fn check_segment(
dir: &DirPath,
first: u64,
path: &DirPath,
limit: Option<u64>,
torn_ok: bool,
chain: Option<&GraphFile>,
findings: &mut Findings,
) -> Option<SegmentCheck> {
let relative = path.relative_to(dir);
match open_segment_reader(path, limit) {
Ok(source) => check_segment_stream(&relative, first, source, torn_ok, chain, findings),
Err(err) => {
let (offset, reason) = located(err);
findings.problem(relative, offset, reason);
None
}
}
}
fn located(err: PersistError) -> (Option<u64>, String) {
match err {
PersistError::Corrupt { offset, reason, .. } => (Some(offset), reason),
other => (None, other.to_string()),
}
}
pub(super) fn check_segment_stream(
relative: &str,
first: u64,
source: impl std::io::Read,
torn_ok: bool,
chain: Option<&GraphFile>,
findings: &mut Findings,
) -> Option<SegmentCheck> {
let relative = relative.to_string();
let mut reader =
match JournalReader::open(source, 0, crate::value::depth::DEFAULT_MAX_VALUE_DEPTH) {
Ok(reader) => reader,
Err(err) => {
let (offset, reason) = located(err);
findings.problem(relative, offset, reason);
return None;
}
};
let before = findings.problems.len();
if reader.first_commit_seq() != first {
findings.problem(
relative.clone(),
Some(28),
format!(
"its header says first commit {}, its name {first}: another segment's bytes",
reader.first_commit_seq()
),
);
}
if let Some(chain) = chain
&& !chain.on_chain(reader.graph_id())
{
findings.problem(
relative.clone(),
Some(12),
"a lineage not on the store's chain".into(),
);
}
let mut check = SegmentCheck {
end: WAL_HEADER_BYTES as u64,
last_record: None,
commits: Vec::new(),
};
loop {
match reader.next_record() {
Ok(Some((start, _, record))) => {
check.last_record = Some(start);
check.end = reader.offset();
if let JournalRecord::Commit(changes) = record {
let seq = changes.commit_seq();
if seq < first {
findings.problem(
relative.clone(),
Some(start),
format!("commit {seq} before the segment's first commit {first}"),
);
}
check.commits.push((start, seq));
}
}
Ok(None) => break,
Err(err) => {
let (offset, reason) = located(err);
findings.problem(relative.clone(), offset, reason);
break;
}
}
}
if let Some(torn) = reader.torn_tail()
&& !torn_ok
{
findings.problem(
relative,
Some(torn.offset),
format!(
"an incomplete record ({} bytes) before the end the backup copies",
torn.bytes
),
);
}
(findings.problems.len() == before).then_some(check)
}
pub(super) fn check_continuity(
segments: &[(u64, &SegmentCheck)],
after: Option<u64>,
findings: &mut Findings,
) {
let mut previous: Option<u64> = None;
for (first, check) in segments {
for (start, seq) in &check.commits {
let expected = match previous {
Some(prev) => Some(prev.saturating_add(1)),
None => after
.filter(|after| *seq > *after)
.map(|after| after.saturating_add(1)),
};
if let Some(expected) = expected
&& *seq != expected
{
findings.problem(
format!("{WAL_DIR}/{}", wal_name(*first)),
Some(*start),
format!("commit {seq} where commit {expected} must follow (a gap in the WAL)"),
);
}
previous = Some(*seq);
}
}
}