diff --git a/apps/figcraft-server/src/store.rs b/apps/figcraft-server/src/store.rs index e2eb6250..86a61288 100644 --- a/apps/figcraft-server/src/store.rs +++ b/apps/figcraft-server/src/store.rs @@ -1243,7 +1243,10 @@ impl Store { Log { generation, file: f, pending: Vec::new(), entries: kept, bytes, dirty: kept > 0 } } // A new snapshot and log of the next generation, so appends start clean. - _ => self.rewrite(file, generation + 1, &server).map_err(|e| e.why)?, + _ => { + let next = generation.checked_add(1).ok_or("the file's generation can't go any higher")?; + self.rewrite(file, next, &server).map_err(|e| e.why)? + } }; self.entry(file)?.log = Some(log); Ok(server) @@ -1319,7 +1322,11 @@ impl Store { fn compact(&mut self, file: &str, server: &Server) { self.compactions += 1; let Some(generation) = self.files.get(file).and_then(|e| e.log.as_ref()).map(|l| l.generation) else { return }; - match self.rewrite(file, generation + 1, server) { + let Some(next) = generation.checked_add(1) else { + log::error!("file {file}: compaction failed: the file's generation can't go any higher"); + return; + }; + match self.rewrite(file, next, server) { Ok(log) => { if let Ok(e) = self.entry(file) { e.log = Some(log); diff --git a/apps/figcraft-server/src/store_tests.rs b/apps/figcraft-server/src/store_tests.rs index ba8f955e..74221455 100644 --- a/apps/figcraft-server/src/store_tests.rs +++ b/apps/figcraft-server/src/store_tests.rs @@ -391,3 +391,30 @@ fn names_and_report_reasons_lose_bidi_controls() { store.remove(&id).unwrap(); } } + +#[test] +fn a_maxed_out_generation_does_not_wrap() { + let dir = TempDir::new("maxgen"); + let c = StoreConfig { compact_after: 3, ..cfg(dir.path()) }; + let mut store = Store::open(dir.path(), c.clone()).unwrap(); + let (id, _, _) = publish(&mut store, "Max"); + let path = dir.path().join(FILES).join(&id); + let (_, state) = disk::decode_snapshot(&fs::read(path.join(SNAPSHOT)).unwrap()).unwrap(); + // Loading: no log goes with the snapshot, so the next generation would overflow. + fs::write(path.join(SNAPSHOT), disk::encode_snapshot(u64::MAX, &state).unwrap()).unwrap(); + fs::remove_file(path.join(LOG)).unwrap(); + assert!(store.open(&id).is_err()); + // Compacting: the open log is at the largest generation. + fs::write(path.join(SNAPSHOT), disk::encode_snapshot(u64::MAX - 1, &state).unwrap()).unwrap(); + let _ = fs::remove_file(path.join(LOG)); + let mut server = store.open(&id).unwrap(); + let mut a = join(&mut server, Access::Edit); + store.changed(&id, &mut server); + for i in 0..4 { + rename(&mut server, &mut a, &format!("P{i}")); + store.changed(&id, &mut server); + store.flush(); + } + let snap = disk::check_snapshot(&fs::read(path.join(SNAPSHOT)).unwrap()).unwrap(); + assert_eq!(snap, u64::MAX); +}