use std::io;
use std::path::Path;
use crate::classify::cert::{Classified, Verdict};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CoveredRange {
pub start: u64,
pub end: u64,
pub tally: [usize; 3],
}
pub fn ranges_path(store: &Path) -> std::path::PathBuf {
store.with_extension("ranges")
}
pub fn read_ranges(store: &Path) -> Vec<CoveredRange> {
let Ok(txt) = std::fs::read_to_string(ranges_path(store)) else {
return Vec::new();
};
let mut out: Vec<CoveredRange> = txt
.lines()
.filter_map(|l| {
let mut it = l.split('\t');
let start = it.next()?.parse().ok()?;
let end = it.next()?.parse().ok()?;
let p = it.next()?.parse().ok()?;
let n = it.next()?.parse().ok()?;
let u = it.next()?.parse().ok()?;
(start < end).then_some(CoveredRange {
start,
end,
tally: [p, n, u],
})
})
.collect();
out.sort_by_key(|r| (r.start, r.end));
out
}
pub(crate) fn append_range(store: &Path, r: CoveredRange) {
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(ranges_path(store))
.unwrap();
writeln!(
f,
"{}\t{}\t{}\t{}\t{}",
r.start, r.end, r.tally[0], r.tally[1], r.tally[2]
)
.unwrap();
f.flush().unwrap();
}
pub fn in_covered_union(union: &[(u64, u64)], idx: u64) -> bool {
match union.partition_point(|&(s, _)| s <= idx) {
0 => false,
i => union[i - 1].1 > idx,
}
}
pub fn covered_union(ranges: &[CoveredRange]) -> Vec<(u64, u64)> {
let mut out: Vec<(u64, u64)> = Vec::new();
for r in ranges {
match out.last_mut() {
Some((_, e)) if r.start <= *e => *e = (*e).max(r.end),
_ => out.push((r.start, r.end)),
}
}
out
}
pub(crate) fn read_resume(
out: &Path,
start: u64,
end: u64,
covered: &[(u64, u64)],
) -> (std::collections::HashSet<u64>, [usize; 3]) {
let mut done = std::collections::HashSet::new();
let mut tally = [0usize; 3];
if !out.exists() {
return (done, tally);
}
let txt = std::fs::read_to_string(out).unwrap_or_else(|e| {
panic!(
"resume: store {} unreadable ({e}); refusing to touch it",
out.display()
)
});
let valid = txt.rfind('\n').map_or(0, |i| i + 1);
let mut keep: Vec<&str> = Vec::new();
let mut seen_any: std::collections::HashSet<u64> = std::collections::HashSet::new();
let mut dropped = 0usize;
for line in txt[..valid].lines() {
let parsed = line.split_once('\t').and_then(|(idx_s, json)| {
let idx = idx_s.parse::<u64>().ok()?;
let c = serde_json::from_str::<Classified>(json).ok()?;
Some((idx, c))
});
let Some((idx, c)) = parsed else {
dropped += 1;
continue;
};
if !seen_any.insert(idx) {
dropped += 1; continue;
}
keep.push(line);
if (start..end).contains(&idx) && !in_covered_union(covered, idx) {
match c {
Classified::Decided(Verdict::Periodic(_)) => tally[0] += 1,
Classified::Decided(Verdict::CannotTile(_)) => tally[1] += 1,
Classified::Undecided { .. } => tally[2] += 1,
}
done.insert(idx);
}
}
let torn = valid as u64
!= std::fs::metadata(out)
.map(|m| m.len())
.unwrap_or(valid as u64);
if dropped > 0 {
eprintln!("resume: dropping {dropped} malformed/duplicate store line(s)");
let tmp = out.with_extension("jsonl.tmp");
{
use std::io::Write;
let mut f = std::io::BufWriter::new(std::fs::File::create(&tmp).unwrap());
for l in &keep {
writeln!(f, "{l}").unwrap();
}
f.flush().unwrap();
}
std::fs::rename(&tmp, out).unwrap();
} else if torn {
let f = std::fs::OpenOptions::new().write(true).open(out).unwrap();
f.set_len(valid as u64).unwrap();
}
(done, tally)
}
pub fn deep_overlay_path(store: &Path) -> std::path::PathBuf {
store.with_extension("deep.jsonl")
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct MergeStats {
pub overridden: usize,
pub kept_decided: usize,
pub still_undecided: usize,
pub appended: usize,
}
pub fn run_merge(store: &Path, overlay: &Path) -> MergeStats {
use std::collections::HashMap;
use std::io::Write;
let otxt = std::fs::read_to_string(overlay).expect("overlay readable");
let mut over: HashMap<u64, (&str, bool)> = HashMap::new(); for line in otxt.lines() {
let Some((idx_s, json)) = line.split_once('\t') else {
continue;
};
let Ok(idx) = idx_s.parse::<u64>() else {
continue;
};
let Ok(c) = serde_json::from_str::<Classified>(json) else {
continue;
};
over.insert(idx, (line, matches!(c, Classified::Decided(_))));
}
let stxt = std::fs::read_to_string(store).expect("store readable");
let mut stats = MergeStats::default();
let tmp = store.with_extension("jsonl.tmp");
{
let mut f = std::io::BufWriter::new(std::fs::File::create(&tmp).unwrap());
for line in stxt.lines() {
let entry = line
.split_once('\t')
.and_then(|(idx_s, json)| {
let idx = idx_s.parse::<u64>().ok()?;
let c = serde_json::from_str::<Classified>(json).ok()?;
Some((idx, c))
})
.and_then(|(idx, c)| over.remove(&idx).map(|o| (c, o)));
match entry {
Some((Classified::Undecided { .. }, (oline, true))) => {
stats.overridden += 1;
writeln!(f, "{oline}").unwrap();
}
Some((Classified::Undecided { .. }, (_, false))) => {
stats.still_undecided += 1;
writeln!(f, "{line}").unwrap();
}
Some((Classified::Decided(_), _)) => {
stats.kept_decided += 1;
writeln!(f, "{line}").unwrap();
}
None => writeln!(f, "{line}").unwrap(),
}
}
for (_, (oline, _)) in over {
stats.appended += 1;
writeln!(f, "{oline}").unwrap();
}
f.flush().unwrap();
}
std::fs::rename(&tmp, store).unwrap();
eprintln!(
"merge: overridden {} / kept-decided {} / still-undecided {} / appended {}{}",
stats.overridden,
stats.kept_decided,
stats.still_undecided,
stats.appended,
if stats.appended > 0 {
" (appends break packing; re-run --pack)"
} else {
""
}
);
stats
}
pub fn run_pack(store: &Path) -> usize {
use std::io::Write;
let txt = std::fs::read_to_string(store).expect("store readable");
let mut lines: Vec<(u64, &str)> = txt
.lines()
.map(|l| {
let idx = l
.split_once('\t')
.and_then(|(i, _)| i.parse::<u64>().ok())
.unwrap_or_else(|| panic!("pack: malformed store line {l:.80}"));
(idx, l)
})
.collect();
lines.sort_unstable_by_key(|&(idx, _)| idx);
let tmp = store.with_extension("jsonl.tmp");
{
let mut f = std::io::BufWriter::new(std::fs::File::create(&tmp).unwrap());
for (_, l) in &lines {
writeln!(f, "{l}").unwrap();
}
f.flush().unwrap();
}
std::fs::rename(&tmp, store).unwrap();
lines.len()
}
fn line_at(f: &mut std::fs::File, pos: u64, file_len: u64) -> io::Result<Option<(u64, String)>> {
use std::io::{BufRead, BufReader, Seek, SeekFrom};
if pos >= file_len {
return Ok(None);
}
let mut start = pos;
let mut r = if pos == 0 {
f.seek(SeekFrom::Start(0))?;
BufReader::new(f)
} else {
f.seek(SeekFrom::Start(pos - 1))?;
let mut r = BufReader::new(f);
let mut skip = String::new();
let n = r.read_line(&mut skip)? as u64;
start = pos - 1 + n;
if start >= file_len {
return Ok(None);
}
r
};
let mut line = String::new();
if r.read_line(&mut line)? == 0 {
return Ok(None);
}
while line.ends_with('\n') || line.ends_with('\r') {
line.pop();
}
Ok(Some((start, line)))
}
fn line_index(line: &str) -> io::Result<u64> {
line.split_once('\t')
.and_then(|(i, _)| i.parse::<u64>().ok())
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("malformed store line: {line:.80}"),
)
})
}
pub fn store_lookup(store: &Path, idx: u64) -> io::Result<Option<Classified>> {
const SCAN_WINDOW: u64 = 64 * 1024;
let mut f = std::fs::File::open(store)?;
let file_len = f.metadata()?.len();
let (mut lo, mut hi) = (0u64, file_len);
while hi - lo > SCAN_WINDOW {
let mid = lo + (hi - lo) / 2;
match line_at(&mut f, mid, file_len)? {
None => hi = mid,
Some((ls, line)) => {
if line_index(&line)? <= idx {
lo = ls; } else {
hi = mid; }
}
}
}
let mut pos = lo;
while let Some((ls, line)) = line_at(&mut f, pos, file_len)? {
let li = line_index(&line)?;
if li == idx {
let json = line.split_once('\t').unwrap().1;
let c = serde_json::from_str::<Classified>(json)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
return Ok(Some(c));
}
if li > idx {
return Ok(None);
}
pos = ls + line.len() as u64 + 1;
}
Ok(None)
}