haematite 0.7.0

Content-addressed, branchable, actor-native storage engine
Documentation
//! The single directory-entry fsync primitive (FENCE-CHAIN r8 R1a).
//!
//! Every directory-entry fsync the fence-chain brief introduces or touches funnels
//! through [`sync_dir_entry`]. Consolidating them in one helper gives the crash
//! battery a single seam to instrument: the FAILPOINT hook (drive the post-mutation
//! refusal tail no real filesystem produces on demand), the LOSSY hook (suppress the
//! real fsync so a still-pending directory-entry mutation survives to the CUT), and
//! the OBSERVER hook (count real syncs by normalised path, so fsync-neutrality on a
//! warm commit is a runnable assertion — R4b).
//!
//! The primitive itself is tiny and production-only: open the directory read-only and
//! `sync_all` it so a rename's directory entry — not just the renamed file's
//! contents — survives power loss. It is Unix-only for the same reason as the
//! pre-existing per-subsystem helpers it supersedes: `std` cannot open a directory
//! for fsync on Windows (`FILE_FLAG_BACKUP_SEMANTICS` is required and unset), so
//! there the directory-entry sync is skipped — a documented platform gap carried as
//! a row in the R2 audit table.

use std::io;
use std::path::Path;

/// fsync the directory entry at `parent`, making a just-renamed or just-created
/// entry inside it durable.
///
/// On success the fence-chain journal (test builds only) clears exactly the
/// directory-entry mutations that were pending against `parent` when the sync
/// began; a mutation registered mid-sync stays pending, so a fence can never
/// falsely certify an entry it did not cover.
#[cfg(unix)]
pub fn sync_dir_entry(parent: &Path) -> io::Result<()> {
    let parent = normalised_parent(parent);

    #[cfg(test)]
    {
        super::journal::record_observed_sync(&parent);
        if super::journal::take_failpoint() {
            return Err(io::Error::other("injected directory-entry fsync failure"));
        }
        if super::journal::is_lossy() {
            // LOSSY: the real fsync is suppressed, so nothing clears — the
            // still-pending mutation survives to the CUT and is rewound.
            return Ok(());
        }
    }

    // Snapshot the pending generations for this parent BEFORE the fsync, then
    // clear exactly those on success — a mutation registered mid-sync stays
    // pending, so a fence can never falsely certify an entry it did not cover.
    #[cfg(test)]
    let snapshot = super::journal::snapshot_pending_generations(&parent);
    std::fs::File::open(&parent).and_then(|dir| dir.sync_all())?;
    #[cfg(test)]
    super::journal::clear_pending_generations(&parent, &snapshot);
    Ok(())
}

/// Non-Unix platforms cannot open a directory for fsync through `std`; the
/// directory-entry sync is skipped there (documented R2 gap). The FAILPOINT hook
/// is still honoured so the crash battery's refusal walls compile and run.
#[cfg(not(unix))]
pub fn sync_dir_entry(parent: &Path) -> io::Result<()> {
    let parent = normalised_parent(parent);

    #[cfg(test)]
    {
        super::journal::record_observed_sync(&parent);
        if super::journal::take_failpoint() {
            return Err(io::Error::other("injected directory-entry fsync failure"));
        }
    }
    let _ = parent;
    Ok(())
}

/// Lexically normalise a parent path so the OBSERVER counts and the journal's
/// per-parent pending sets key identically whether a caller passed an absolute
/// path, a relative one, or `""` (mapped to `.`). Never touches the filesystem —
/// the target may already be gone by the time a CUT normalises it.
pub(super) fn normalised_parent(parent: &Path) -> std::path::PathBuf {
    let parent = if parent.as_os_str().is_empty() {
        Path::new(".")
    } else {
        parent
    };
    std::path::absolute(parent).unwrap_or_else(|_error| parent.to_path_buf())
}