libfreemkv 1.1.0

Open source raw disc access library for optical drives
Documentation
//! Event system for progress and status reporting.
//!
//! The lib fires events during operations like rip().
//! The app registers a callback to receive them.
//! No display logic, no English text — just data.
//!
//! ```rust,ignore
//! disc.rip(&mut session, 0, output, |event| {
//!     match event.kind {
//!         EventKind::BytesRead { bytes, total } => update_progress(bytes, total),
//!         EventKind::SectorSkipped { sector } => log_skip(sector),
//!         EventKind::BatchSizeChanged { new_size, .. } => note_recovery(new_size),
//!         _ => {}
//!     }
//! });
//! ```
//!
//! Note: the library currently emits only `BytesRead`, `SectorSkipped`,
//! and `BatchSizeChanged`. The other [`EventKind`] variants are part of
//! the stable event vocabulary for consumers (and future emit sites) but
//! are not produced by the library today.

use crate::error::Error;

/// An event fired by the lib during operations.
#[derive(Debug)]
pub struct Event {
    pub kind: EventKind,
}

/// Types of events the lib can fire.
#[derive(Debug)]
pub enum EventKind {
    // ── Init sequence events ────────────────────────────────────────
    /// Drive opened successfully.
    DriveOpened { device: String },

    /// Drive is ready (disc spun up).
    DriveReady,

    /// Firmware init completed.
    InitComplete { success: bool },

    /// Disc probe completed.
    ProbeComplete { success: bool },

    /// Disc scan completed.
    ScanComplete { titles: usize },

    // ── Read events ─────────────────────────────────────────────────
    /// Bytes successfully read and written to output.
    BytesRead {
        /// Bytes written so far.
        bytes: u64,
        /// Total bytes expected (0 if unknown).
        total: u64,
    },

    /// A read error occurred. The lib will retry automatically.
    ReadError {
        /// Sector that failed.
        sector: u64,
        /// Error code.
        error: Error,
    },

    /// Retrying a failed read.
    Retry {
        /// Current attempt number (1-based).
        attempt: u32,
    },

    /// Drive speed changed (error recovery or restoration).
    SpeedChange {
        /// New speed in KB/s (0xFFFF = max).
        speed_kbs: u16,
    },

    /// Starting a new disc extent.
    ExtentStart {
        /// Extent index (0-based).
        index: usize,
        /// First sector of extent.
        start_sector: u64,
        /// Number of sectors in extent.
        sector_count: u64,
    },

    /// Sector recovered after a retry (Drive::read multi-phase recovery).
    SectorRecovered { sector: u64 },

    /// Sector unreadable, zero-filled (skip mode).
    SectorSkipped { sector: u64 },

    /// Adaptive batch sizer changed the read size.
    ///
    /// Fires on shrink (read failed at larger size) and on probe-up
    /// (enough clean reads to try larger again). Consumers use this to
    /// display a "recovering" state distinct from "ripping normally".
    BatchSizeChanged {
        new_size: u16,
        reason: BatchSizeReason,
    },

    /// Operation complete.
    Complete {
        /// Total bytes written.
        bytes: u64,
        /// Total read errors encountered.
        errors: u32,
    },
}

/// Why the adaptive batch sizer changed size.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BatchSizeReason {
    /// Read failed; sizer halved the batch.
    Shrunk,
    /// Clean-read streak threshold hit; sizer doubled toward preferred.
    Probed,
}

/// A no-op event handler. Ignores all events.
pub fn ignore(_event: Event) {}

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

    /// BatchSizeReason::Shrunk != BatchSizeReason::Probed.
    /// These two variants carry distinct meanings (error vs. recovery); they
    /// must not compare as equal.
    /// Mutation: deriving PartialEq without proper variant discrimination
    ///           could make two distinct variants equal.
    #[test]
    fn batch_size_reason_variants_are_not_equal() {
        assert_ne!(BatchSizeReason::Shrunk, BatchSizeReason::Probed);
    }

    /// BatchSizeReason is Clone + Copy: cloning does not move the original.
    /// This is required because EventKind::BatchSizeChanged embeds it by value.
    /// Mutation: removing Copy would require the caller to clone explicitly;
    ///           code that passes reason by value would fail to compile.
    #[test]
    fn batch_size_reason_is_copy() {
        let r = BatchSizeReason::Shrunk;
        let _r2 = r; // copy, not move
        let _r3 = r; // r still usable after copy
    }
}