weavatrix-worktree 0.2.1

Bounded, crash-recoverable multi-file worktree edits for source refactoring
Documentation
use crate::{
    error::{TransactionPhase, WorktreeError, WorktreeErrorCode},
    filesystem::{FileIdentity, TargetAccess},
    hash::Sha256Hash,
};

use super::util::fs_error;

pub(crate) fn verify_target(
    access: &TargetAccess,
    expected_hash: Sha256Hash,
    expected_identity: Option<FileIdentity>,
    max_bytes: usize,
    index: usize,
    phase: TransactionPhase,
) -> Result<(), WorktreeError> {
    let snapshot = access.snapshot(max_bytes).map_err(|error| {
        fs_error(
            phase,
            access.path(),
            index,
            "failed to revalidate a transaction target",
            error,
        )
    })?;
    if expected_identity.is_some_and(|identity| identity != snapshot.identity) {
        return Err(concurrent(access, index, phase, "target identity changed"));
    }
    let actual = Sha256Hash::compute(&snapshot.source);
    if actual != expected_hash {
        return Err(concurrent(
            access,
            index,
            phase,
            &format!("expected target {expected_hash}, found {actual}"),
        ));
    }
    Ok(())
}

pub(crate) fn verify_artifact(
    access: &TargetAccess,
    name: &str,
    expected_hash: Sha256Hash,
    max_bytes: usize,
    index: usize,
    phase: TransactionPhase,
) -> Result<(), WorktreeError> {
    let bytes = access.read_artifact(name, max_bytes).map_err(|error| {
        let error = fs_error(
            phase,
            access.path(),
            index,
            "failed to read a transaction artifact",
            error,
        );
        if phase == TransactionPhase::Commit {
            error
        } else {
            error.requiring_recovery()
        }
    })?;
    if Sha256Hash::compute(&bytes) != expected_hash {
        let code = if phase == TransactionPhase::Commit {
            WorktreeErrorCode::CommitFailed
        } else {
            WorktreeErrorCode::RecoveryRequired
        };
        let error = WorktreeError::new(
            code,
            phase,
            "transaction artifact SHA-256 does not match its journal evidence",
        )
        .at_path(access.path().to_owned())
        .at_file(index);
        return Err(if phase == TransactionPhase::Commit {
            error
        } else {
            error.requiring_recovery()
        });
    }
    Ok(())
}

fn concurrent(
    access: &TargetAccess,
    index: usize,
    phase: TransactionPhase,
    message: &str,
) -> WorktreeError {
    WorktreeError::new(WorktreeErrorCode::ConcurrentModification, phase, message)
        .at_path(access.path().to_owned())
        .at_file(index)
}