use super::fs::SimFs;
use crate::coordinate::Coordinate;
use crate::event::EventKind;
use crate::store::fork_report::{ForkFinding, ForkOptions};
use crate::store::{Open, Store, StoreConfig, StoreError};
use std::path::Path;
use std::sync::Arc;
fn build_source(
source_dir: &Path,
sim_fs: &Arc<SimFs>,
events: usize,
) -> Result<Store<Open>, String> {
let config = StoreConfig::new(source_dir)
.with_sync_every_n_events(1)
.with_segment_max_bytes(512)
.with_fs(Arc::clone(sim_fs) as Arc<dyn crate::store::platform::fs::StoreFs>);
let store = Store::<Open>::open(config).map_err(|e| format!("open source: {e}"))?;
let kind = EventKind::custom(0xF, 0x0B);
for i in 0..events {
let coord = Coordinate::new(format!("entity-{i}"), "scope:fork-hostile")
.map_err(|e| format!("coord: {e}"))?;
let _receipt = store
.append(&coord, kind, &serde_json::json!({ "n": i }))
.map_err(|e| format!("append: {e}"))?;
}
crate::store::lifecycle::sync(&store).map_err(|e| format!("sync: {e}"))?;
Ok(store)
}
fn dest_is_published_store(dest: &Path) -> bool {
if !dest.exists() {
return false;
}
match Store::open_read_only(StoreConfig::new(dest)) {
Ok(store) => store.stats().event_count > 0,
Err(_) => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SymlinkDestOutcome {
pub refused: bool,
pub no_publish: bool,
}
pub fn run_fork_symlink_dest(
link_dest: &Path,
real_target: &Path,
) -> Result<SymlinkDestOutcome, String> {
let dir = tempfile::tempdir().map_err(|e| format!("tmpdir: {e}"))?;
let source_dir = dir.path().join("source");
let sim_fs = Arc::new(SimFs::new(0x5117_0001, 0));
let store = build_source(&source_dir, &sim_fs, 3)?;
let result = store.fork_with_evidence(link_dest, ForkOptions::default());
let refused = matches!(result, Err(StoreError::Io(_)));
let no_publish = !dest_is_published_store(real_target);
store.close().map_err(|e| format!("close: {e}"))?;
Ok(SymlinkDestOutcome {
refused,
no_publish,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DestEqualsSourceOutcome {
pub refused: bool,
pub refused_invalid_input: bool,
}
pub fn run_fork_dest_equals_source() -> Result<DestEqualsSourceOutcome, String> {
let dir = tempfile::tempdir().map_err(|e| format!("tmpdir: {e}"))?;
let source_dir = dir.path().join("source");
let sim_fs = Arc::new(SimFs::new(0x5117_0002, 0));
let store = build_source(&source_dir, &sim_fs, 3)?;
let result = store.fork_with_evidence(&source_dir, ForkOptions::default());
let refused = result.is_err();
let refused_invalid_input = matches!(
&result,
Err(StoreError::Io(io_err)) if io_err.kind() == std::io::ErrorKind::InvalidInput
);
store.close().map_err(|e| format!("close: {e}"))?;
Ok(DestEqualsSourceOutcome {
refused,
refused_invalid_input,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaleDestOutcome {
pub forked_ok: bool,
pub cleared_stale: bool,
pub dest_matches_source: bool,
}
pub const STALE_SEGMENT_FILE: &str = "000099.fbat";
pub const STALE_RANGES_FILE: &str = "visibility_ranges.fbv";
pub fn run_fork_stale_dest(dest_dir: &Path) -> Result<StaleDestOutcome, String> {
let dir = tempfile::tempdir().map_err(|e| format!("tmpdir: {e}"))?;
let source_dir = dir.path().join("source");
let sim_fs = Arc::new(SimFs::new(0x5117_0003, 0));
let store = build_source(&source_dir, &sim_fs, 4)?;
let source_committed = store.stats().event_count;
let report = store
.fork_with_evidence(dest_dir, ForkOptions::default())
.map_err(|e| format!("fork over stale dest: {e}"))?;
let forked_ok = true;
let cleared_stale = report
.body
.findings
.iter()
.any(|f| matches!(f, ForkFinding::DestinationCleared { .. }));
let dest_count = match Store::open_read_only(StoreConfig::new(dest_dir)) {
Ok(forked) => forked.stats().event_count,
Err(e) => return Err(format!("reopen forked dest: {e}")),
};
let dest_matches_source = dest_count == source_committed;
store.close().map_err(|e| format!("close: {e}"))?;
Ok(StaleDestOutcome {
forked_ok,
cleared_stale,
dest_matches_source,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EnospcMidCopyOutcome {
pub refused: bool,
pub refused_storage_full: bool,
pub no_partial_publish: bool,
}
pub fn run_fork_enospc_mid_copy() -> Result<EnospcMidCopyOutcome, String> {
let dir = tempfile::tempdir().map_err(|e| format!("tmpdir: {e}"))?;
let source_dir = dir.path().join("source");
let dest_dir = dir.path().join("dest");
let sim_fs = Arc::new(SimFs::new(0x5117_0004, 0).with_enospc_on_copy(1));
let store = build_source(&source_dir, &sim_fs, 8)?;
let result = store.fork_with_evidence(&dest_dir, ForkOptions::default());
let refused = result.is_err();
let refused_storage_full = matches!(
&result,
Err(StoreError::Io(io_err)) if io_err.kind() == std::io::ErrorKind::StorageFull
);
let no_partial_publish = !dest_is_published_store(&dest_dir);
store.close().map_err(|e| format!("close: {e}"))?;
Ok(EnospcMidCopyOutcome {
refused,
refused_storage_full,
no_partial_publish,
})
}
#[cfg(all(test, feature = "dangerous-test-hooks"))]
mod tests {
use super::*;
#[test]
fn dest_is_published_store_is_true_only_for_a_real_nonempty_store() {
let dir = tempfile::tempdir().expect("tmpdir");
let store_dir = dir.path().join("published");
{
let store = Store::<Open>::open(StoreConfig::new(&store_dir)).expect("open store");
let coord = Coordinate::new("entity:pub", "scope:published").expect("coord");
let _receipt = store
.append(
&coord,
EventKind::custom(0xF, 0x0B),
&serde_json::json!({ "n": 1 }),
)
.expect("append one event");
crate::store::lifecycle::sync(&store).expect("sync");
store.close().expect("close");
}
assert!(
dest_is_published_store(&store_dir),
"a real, synced, non-empty store must classify as a published fork destination"
);
let missing = dir.path().join("nothing-here");
assert!(
!dest_is_published_store(&missing),
"a nonexistent destination must NOT classify as a published store"
);
}
}