Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cd-da-reader"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
description = "CD-DA (audio CD) reading library"
repository = "https://github.com/Bloomca/rust-cd-da-reader"
Expand Down
21 changes: 21 additions & 0 deletions src/discovery.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
use crate::{CdReader, CdReaderError};

/// Information about all found drives. This info is not tested extensively, and in
/// general it is encouraged to provide a disk drive directly.
#[derive(Debug, Clone)]
pub struct DriveInfo {
/// Path to the drive, which can be something like 'disk6' on macOS,
/// '\\.\E:' on Windows, and '/dev/sr0' on Linux
pub path: String,
/// Just the device name, without the full path for the OS
pub display_name: Option<String>,
/// We load the disc and issue a TOC command, which is supported only on media CDs
pub has_audio_cd: bool,
}

impl CdReader {
/// Enumerate candidate optical drives and probe whether they currently have an audio CD.
///
/// This method does not work on macOS due to the fact for reliable confirmation
/// whether the drive has an Audio CD we need to mount it and later release; macOS
/// can assign a different drive name afterwards, so reading that name is unreliable.
/// Instead, use open_drive(drive) or open_default(), which acquires the exclusivity
#[cfg(target_os = "macos")]
pub fn list_drives() -> Result<Vec<DriveInfo>, CdReaderError> {
Err(CdReaderError::Io(std::io::Error::other(
Expand All @@ -17,6 +28,9 @@ impl CdReader {
}

/// Enumerate candidate optical drives and probe whether they currently have an audio CD.
///
/// On Windows, we try to read type of every drive from A to Z. On Linux, we read
/// /sys/class/block directory and check every entry starting with "sr"
#[cfg(not(target_os = "macos"))]
pub fn list_drives() -> Result<Vec<DriveInfo>, CdReaderError> {
let mut paths = {
Expand Down Expand Up @@ -60,6 +74,10 @@ impl CdReader {
}

/// Open the first discovered drive that currently has an audio CD.
///
/// On macOS, we open each drive returned from `diskutil list`, and
/// evaluate each disk. Once we are able to open it and read correct TOC,
/// we return it back with already acquired exclusivity.
#[cfg(target_os = "macos")]
pub fn open_default() -> Result<Self, CdReaderError> {
let mut paths = crate::macos::list_drive_paths().map_err(CdReaderError::Io)?;
Expand All @@ -85,6 +103,9 @@ impl CdReader {
}

/// Open the first discovered drive that currently has an audio CD.
///
/// On Windows and Linux, we get the first device from the list and
/// try to open it, returning an error if it fails.
#[cfg(not(target_os = "macos"))]
pub fn open_default() -> Result<Self, CdReaderError> {
let drives = Self::list_drives()?;
Expand Down
19 changes: 19 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
use std::fmt;

/// SCSI command groups issued by this library.
#[derive(Debug, Clone, Copy)]
pub enum ScsiOp {
/// `READ TOC/PMA/ATIP` command (opcode `0x43`) for TOC/session metadata.
ReadToc,
/// `READ CD` command (opcode `0xBE`) for CD-DA sector payload (2352 bytes/sector).
ReadCd,
/// `READ SUB-CHANNEL` command for Q-channel/subcode metadata.
ReadSubChannel,
}

/// Structured SCSI failure context captured at the call site.
///
/// This keeps transport/protocol details (status + sense) separate from plain I/O failures,
/// which allows retry logic and application diagnostics to branch on SCSI metadata.
#[derive(Debug, Clone)]
pub struct ScsiError {
/// Operation that failed.
pub op: ScsiOp,
/// Starting logical block address used by the failed command, when applicable.
pub lba: Option<u32>,
/// Sector count requested by the failed command, when applicable.
pub sectors: Option<u32>,
/// SCSI status byte reported by the device (for example `0x02` for CHECK CONDITION).
pub scsi_status: u8,
/// Sense key nibble from fixed-format sense data (if sense data was returned).
pub sense_key: Option<u8>,
/// Additional Sense Code from sense data (if available).
pub asc: Option<u8>,
/// Additional Sense Code Qualifier paired with `asc` (if available).
pub ascq: Option<u8>,
}

/// Top-level error type returned by `cd-da-reader`.
#[derive(Debug)]
pub enum CdReaderError {
/// OS/transport I/O error (open/ioctl/DeviceIoControl/FFI command failure, etc.).
Io(std::io::Error),
/// Device reported a SCSI command failure with status/sense context.
Scsi(ScsiError),
/// Parsing failure for command payloads (TOC/CD-TEXT/subchannel parsing).
Parse(String),
}

Expand Down
22 changes: 22 additions & 0 deletions src/retry.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
/// Retry policy for idempotent read operations.
///
/// The policy is applied per failed read chunk/command and can combine
/// capped exponential backoff with adaptive chunk-size reduction.
#[derive(Debug, Clone)]
pub struct RetryConfig {
/// Maximum attempts per operation, including the initial attempt.
/// By default the value is 4.
///
/// Values below `1` are normalized by callers to at least one attempt.
pub max_attempts: u8,
/// Initial backoff delay in milliseconds before the second attempt.
/// First attempt is always immediate, so if there are no issues during
/// reading, we don't wait any time.
pub initial_backoff_ms: u64,
/// Upper bound for exponential backoff delay in milliseconds.
///
/// Each retry typically doubles the previous delay until this cap is reached.
pub max_backoff_ms: u64,
/// Enable adaptive sector-count reduction on retry for `READ CD` operations.
///
/// Current implementation reduces chunk size from large reads toward smaller
/// reads (for example `27 -> 8 -> 1`) to have a higher change of success.
pub reduce_chunk_on_retry: bool,
/// Minimum sectors per `READ CD` command when adaptive reduction is enabled.
/// Default value is 27 for 64KB per read.
///
/// Use `1` for maximal fault isolation; larger values can improve throughput.
pub min_sectors_per_read: u32,
}

Expand Down