docs(config): the quarantine cap wraps, and the numbered copies never move

`quarantine_path` ends in

    candidates.find(|c| !c.exists()).unwrap_or(base)

which reads as a defensive branch for a case that cannot happen. It is
the wrap. Eight is a cap on how much of a repeatedly-corrupted file's
history a config directory may accumulate, and once every name is taken
the oldest copy is the one overwritten.

The part worth writing down is what that leaves behind: nothing rotates
the numbered names, so from the cap onward `.corrupt.1` through
`.corrupt.7` hold the second through eighth corruptions forever and only
the base name moves. Read as a timeline — which is how someone
recovering a tree would read it — that is the opposite of what it looks
like.

Pinned rather than asserted: nine corruptions, eight files left, the
ninth landing on the base, and the base holding the ninth's contents.

Found by sweeping numeric constants for ones whose *value* is
load-bearing. Most of the 367 undocumented ones are self-evident (font
size bounds, test timeouts); this was the one where the number decides
what a user can still recover.
This commit is contained in:
l0ng-ai
2026-08-16 08:23:18 +08:00
parent cadf4a2b3b
commit e1ca32eb77
+64
View File
@@ -929,6 +929,17 @@ fn quarantine_candidates(path: &std::path::Path) -> impl Iterator<Item = PathBuf
.chain((1..MAX_QUARANTINED).map(|n| path.with_extension(format!("json.corrupt.{n}"))))
}
/// Where the next quarantined copy of `path` goes.
///
/// The first free name, and once every name is taken the base one again — so
/// `unwrap_or(base)` is the wrap, not a fallback for something that cannot
/// happen. Eight is a cap on how much of a repeatedly-corrupted file's history
/// a config directory may accumulate; past it the oldest copy is the one
/// overwritten, and `.corrupt.1` through `.corrupt.7` keep the second through
/// eighth corruptions from then on.
///
/// Nothing rotates the numbered names, which is worth knowing before reading
/// them as a timeline: after the cap is reached only the base name moves.
fn quarantine_path(path: &std::path::Path) -> PathBuf {
let mut candidates = quarantine_candidates(path);
let base = candidates.next().expect("the base name is always offered");
@@ -1159,6 +1170,59 @@ where
mod tests {
use super::*;
/// What a ninth corruption does to the eight kept copies.
///
/// `quarantine_path` ends in `unwrap_or(base)`, which reads like a
/// defensive branch and is the wrap: the cap is on how much history a
/// config directory may accumulate, and the oldest copy is what gives way.
/// The numbered names never rotate, so anyone reading them as a timeline
/// after the cap would be reading the second-to-eighth corruptions and a
/// base that has been overwritten however many times since.
#[test]
fn a_ninth_corruption_overwrites_the_oldest_kept_copy() {
let dir = std::env::temp_dir().join(format!("tty7-quar-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("scratch dir");
let path = dir.join("machine.json");
let mut used = Vec::new();
for n in 0..9 {
std::fs::write(&path, format!("corruption {n}")).expect("write the file");
let aside = quarantine_path(&path);
used.push(
aside
.file_name()
.and_then(|n| n.to_str())
.expect("a utf-8 name")
.to_string(),
);
quarantine_by_rename(&path);
}
assert_eq!(
used[0], "machine.json.corrupt",
"the first goes to the base"
);
assert_eq!(used[1], "machine.json.corrupt.1");
assert_eq!(used[7], "machine.json.corrupt.7", "eight distinct slots");
assert_eq!(
used[8], "machine.json.corrupt",
"the ninth wraps onto the oldest rather than growing the directory"
);
let kept = std::fs::read_dir(&dir)
.expect("read the dir")
.filter_map(Result::ok)
.filter(|e| e.file_name().to_string_lossy().contains(".corrupt"))
.count();
assert_eq!(kept, 8, "the cap holds: nine corruptions, eight files");
let base = std::fs::read_to_string(path.with_extension("json.corrupt"))
.expect("the base copy survives");
assert_eq!(base, "corruption 8", "and holds the newest, not the first");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn profile_usage_score_ranks_frequency_and_recency() {
let now = 100_000_000u64;