#![allow(
clippy::cast_possible_truncation,
reason = "mutation offsets are reduced modulo tiny lengths"
)]
use std::time::{Duration, Instant};
use gedcomkit::{Document, Limits, decode_gedcom};
struct Rng(u64);
impl Rng {
const fn next(&mut self) -> u64 {
let mut value = self.0;
value ^= value >> 12;
value ^= value << 25;
value ^= value >> 27;
self.0 = value;
value.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
const fn below(&mut self, ceiling: u64) -> u64 {
if ceiling == 0 {
0
} else {
self.next() % ceiling
}
}
}
fn seeds() -> Vec<Vec<u8>> {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/vendored");
let mut seeds = Vec::new();
for directory in ["gedcom7code/5", "gedcom7code/7", "zip-interop"] {
let Ok(entries) = std::fs::read_dir(root.join(directory)) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
let extension = path
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or_default();
if matches!(extension, "ged" | "gdz")
&& let Ok(bytes) = std::fs::read(&path)
{
seeds.push(bytes);
}
}
}
assert!(seeds.len() > 40, "the vendored corpus is the seed set");
seeds
}
fn mutate(rng: &mut Rng, input: &mut Vec<u8>, other: &[u8]) {
if input.is_empty() {
input.extend_from_slice(&other[..other.len().min(64)]);
return;
}
match rng.below(6) {
0 => {
let at = rng.below(input.len() as u64) as usize;
input[at] ^= (rng.next() & 0xFF) as u8;
}
1 => {
let at = rng.below(input.len() as u64) as usize;
input[at] = (rng.next() & 0xFF) as u8;
}
2 => {
let keep = rng.below(input.len() as u64) as usize;
input.truncate(keep);
}
3 => {
let at = rng.below(input.len() as u64) as usize;
let run = (rng.below(16) + 1) as usize;
let piece: Vec<u8> = input[at..input.len().min(at + run)].to_vec();
input.splice(at..at, piece);
}
4 => {
let from = rng.below(other.len() as u64) as usize;
let run = (rng.below(64) + 1) as usize;
let piece = &other[from..other.len().min(from + run)];
let at = rng.below(input.len() as u64) as usize;
input.splice(at..at, piece.iter().copied());
}
_ => {
let at = rng.below(input.len() as u64) as usize;
input.insert(
at,
[b'\n', b'\r', b'@', b'0', b' ', 0xFF][rng.below(6) as usize],
);
}
}
}
#[test]
#[ignore = "the long adversarial run; set GEDCOM_ENDURANCE_SECONDS and run explicitly"]
fn mutation_endurance_holds_every_invariant() {
let budget = std::env::var("GEDCOM_ENDURANCE_SECONDS")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(300);
let deadline = Instant::now() + Duration::from_secs(budget);
let limits = Limits::DEFAULT
.with_input_bytes(4 << 20)
.with_records(100_000)
.with_structures(1_000_000);
let seeds = seeds();
let mut rng = Rng(0x0F0A_57EE_D000_0001 | 1);
let mut input = Vec::new();
let mut cases = 0u64;
let mut accepted = 0u64;
while Instant::now() < deadline {
let seed = &seeds[rng.below(seeds.len() as u64) as usize];
let other = &seeds[rng.below(seeds.len() as u64) as usize];
input.clear();
input.extend_from_slice(seed);
for _ in 0..=rng.below(8) {
mutate(&mut rng, &mut input, other);
}
if input.len() > (4 << 20) {
input.truncate(4 << 20);
}
cases += 1;
#[cfg(feature = "gedzip")]
if let Ok(archive) = gedcomkit::gedzip::Archive::open_with(
&input,
gedcomkit::gedzip::ArchiveLimits::DEFAULT
.with_entries(1_000)
.with_entry_bytes(1 << 20)
.with_total_bytes(1 << 24),
) {
let _ = archive.document_name();
for entry in archive.entries().to_vec() {
let _ = archive.read_entry(&entry);
}
}
let Ok((text, _report)) = gedcomkit::decode::decode_gedcom_with(&input, limits) else {
continue;
};
let Ok(document) = Document::parse_with(&text, limits) else {
continue;
};
accepted += 1;
let written = document.to_text();
let again =
Document::parse_with(&written, limits).expect("what the writer produced must parse");
assert_eq!(
again.to_text(),
written,
"write→parse→write moved on case {cases}"
);
for record in &document.records {
let _ = gedcomkit::view::RecordView::from_node(record);
}
let _ = decode_gedcom(written.as_bytes());
}
println!(
"endurance: {cases} mutated cases over {budget}s, {accepted} accepted and round-tripped"
);
assert!(cases > 1_000, "the run must actually have run: {cases}");
}