zipatch-rs 1.6.0

Parser for FFXIV ZiPatch patch files
Documentation
//! Standalone cancellation primitive for apply-time aborts.
//!
//! [`CancelToken`] is a cheap, cloneable handle wrapping an
//! [`AtomicBool`](std::sync::atomic::AtomicBool). One handle is held by the
//! thread driving the apply ([`ApplyConfig::apply_patch`](crate::ApplyConfig::apply_patch)
//! or [`IndexApplier::execute`](crate::IndexApplier::execute)) and one or
//! more clones live wherever cancellation is initiated — typically a UI
//! event handler or a `tokio::select!` arm.
//!
//! # Why a separate primitive
//!
//! Cancellation can also be signalled by an [`ApplyObserver`](crate::ApplyObserver)
//! that returns `true` from [`ApplyObserver::should_cancel`](crate::ApplyObserver::should_cancel),
//! but that route forces a consumer who only wants cancellation to implement
//! the entire trait. The token makes the common case trivial:
//!
//! ```no_run
//! use zipatch_rs::{ApplyConfig, CancelToken, open_patch};
//!
//! let token = CancelToken::new();
//! let bg = token.clone();
//! std::thread::spawn(move || {
//!     // Some other thread flips the flag on a user gesture.
//!     bg.cancel();
//! });
//!
//! let reader = open_patch("patch.patch").unwrap();
//! let _ = ApplyConfig::new("/opt/ffxiv/game")
//!     .with_cancel_token(token)
//!     .apply_patch(reader);
//! ```
//!
//! When both a token and an observer are installed, the apply driver polls
//! both — either source can request cancellation independently, and they
//! compose without a wrapper observer.

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

/// Cloneable cancellation flag shared between the apply worker and any
/// number of signalling threads.
///
/// `Clone` is `O(1)` — every handle points at the same underlying
/// [`AtomicBool`]. Marking the token cancelled from any handle is visible to
/// every other handle on the next [`Self::is_cancelled`] poll.
///
/// # Threading
///
/// `Send + Sync` is automatic: the inner [`Arc`] of [`AtomicBool`] is
/// trivially shareable across threads. Construct on one thread, clone, and
/// hand the clones to wherever they need to live.
///
/// # Cost
///
/// [`Self::is_cancelled`] is a single [`Ordering::Relaxed`] atomic load and
/// is cheap enough to call from the inner SQPK block loop. [`Self::cancel`]
/// is one relaxed store. The token allocates one [`Arc`]-managed
/// [`AtomicBool`] at construction; clones share it.
#[derive(Clone, Debug, Default)]
pub struct CancelToken(Arc<AtomicBool>);

impl CancelToken {
    /// Construct a fresh, un-cancelled token.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Request cancellation.
    ///
    /// Idempotent — calling this on an already-cancelled token is a no-op.
    /// The flip is immediately visible to every clone of this token.
    pub fn cancel(&self) {
        self.0.store(true, Ordering::Relaxed);
    }

    /// Returns `true` if [`Self::cancel`] has been called on any clone of
    /// this token.
    ///
    /// Uses [`Ordering::Relaxed`] — the apply driver polls this in hot
    /// loops and only needs eventual visibility, not synchronisation with
    /// any other memory ops.
    #[must_use]
    pub fn is_cancelled(&self) -> bool {
        self.0.load(Ordering::Relaxed)
    }

    /// Clear the cancellation flag.
    ///
    /// Useful for retry-after-cancel scenarios where the consumer wants to
    /// reuse the same token (and therefore avoid re-wiring its clones)
    /// across multiple apply attempts. The flip is immediately visible to
    /// every clone.
    pub fn reset(&self) {
        self.0.store(false, Ordering::Relaxed);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_token_is_not_cancelled() {
        let t = CancelToken::new();
        assert!(!t.is_cancelled());
    }

    #[test]
    fn cancel_flips_the_flag() {
        let t = CancelToken::new();
        t.cancel();
        assert!(t.is_cancelled());
    }

    #[test]
    fn cancel_is_visible_across_clones() {
        let a = CancelToken::new();
        let b = a.clone();
        assert!(!b.is_cancelled());
        a.cancel();
        assert!(
            b.is_cancelled(),
            "cancel on one handle must surface on the clone"
        );
    }

    #[test]
    fn cancel_is_idempotent() {
        let t = CancelToken::new();
        t.cancel();
        t.cancel();
        assert!(t.is_cancelled());
    }

    #[test]
    fn reset_clears_the_flag() {
        let t = CancelToken::new();
        t.cancel();
        assert!(t.is_cancelled());
        t.reset();
        assert!(!t.is_cancelled());
    }

    #[test]
    fn reset_is_visible_across_clones() {
        let a = CancelToken::new();
        let b = a.clone();
        a.cancel();
        assert!(b.is_cancelled());
        b.reset();
        assert!(
            !a.is_cancelled(),
            "reset on one handle must surface on the clone"
        );
    }

    #[test]
    fn cancel_propagates_between_threads() {
        let t = CancelToken::new();
        let bg = t.clone();
        let handle = std::thread::spawn(move || {
            bg.cancel();
        });
        handle.join().unwrap();
        assert!(t.is_cancelled());
    }

    #[test]
    fn token_is_send_and_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<CancelToken>();
    }
}