From 283a561c12e4a73b8cce24f9e23f1cb314cd4fda Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 9 May 2026 09:04:16 -0700 Subject: [PATCH] 0.18 primitive: SectorSource/SectorSink trait split + DecryptingSectorSource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the unidirectional read trait from a (planned) write trait at the sector level, eliminating runtime "wrong direction" potential. Keeps SectorReader alive as a pre-deprecation alias via blanket impl so existing callers compile unchanged through the migration window. Adds DecryptingSectorSource decorator: wrap any SectorSource in this to get plaintext sectors out. Replaces the duplicate decrypt code paths in sweep_pipeline and DiscStream (those migrations are follow-up commits). The formal #[deprecated] attribute on SectorReader is held back to a follow-up commit because internal call sites in disc/, udf/, mux/, and verify/ still go through the legacy trait, and the CI gauntlet treats deprecation lints as errors. Behavioural intent — "this trait is going away" — is documented on the trait itself. See (internal)/memory/0_18_redesign.md. Single contributor: MattJackson. --- src/lib.rs | 16 ++- src/sector.rs | 82 ------------- src/sector/decrypting.rs | 190 ++++++++++++++++++++++++++++++ src/sector/file.rs | 245 +++++++++++++++++++++++++++++++++++++++ src/sector/mod.rs | 154 ++++++++++++++++++++++++ 5 files changed, 601 insertions(+), 86 deletions(-) delete mode 100644 src/sector.rs create mode 100644 src/sector/decrypting.rs create mode 100644 src/sector/file.rs create mode 100644 src/sector/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 6607162..cff6be5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -170,10 +170,18 @@ pub use mux::{InputOptions, StreamUrl, input, output, parse_url}; // ─── Lower-level surfaces ─────────────────────────────────────────────────── // // `ScsiTransport` is the platform-abstraction trait Drive uses; expose for -// out-of-tree platform backends. `SectorReader` lets callers feed any byte -// source (test harness, network image, SMB share) into the disc scan -// pipeline; `FileSectorReader` is the standard ISO-on-disk implementation. +// out-of-tree platform backends. `SectorSource` / `SectorSink` are the 0.18 +// direction-typed read/write traits; `FileSectorSource` and `FileSectorSink` +// are the ISO-on-disk implementations. [`DecryptingSectorSource`] is the +// single decrypt-on-read decorator (AACS / CSS / none) — wrap any +// `SectorSource` to get plaintext sectors out. The legacy `SectorReader` / +// `FileSectorReader` names stay re-exported through the 0.18 migration +// window so existing call sites compile unchanged; a blanket impl makes +// every `SectorReader` automatically usable as a `SectorSource`. pub use scsi::{DriveInfo, ScsiSense, ScsiTransport, drive_has_disc, list_drives}; -pub use sector::{FileSectorReader, SectorReader}; +pub use sector::{ + DecryptingSectorSource, FileSectorReader, FileSectorSink, FileSectorSource, SectorReader, + SectorSink, SectorSource, +}; pub use speed::DriveSpeed; pub use udf::{UdfFs, read_filesystem}; diff --git a/src/sector.rs b/src/sector.rs deleted file mode 100644 index 4adc843..0000000 --- a/src/sector.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! SectorReader — trait for reading 2048-byte disc sectors. -//! -//! Implemented by Drive (SCSI) and IsoFile (file-backed). -//! Used by UDF parser, disc scanner, label parsers — anything that -//! reads sectors doesn't need to know where they come from. - -use crate::error::Result; - -/// Read 2048-byte sectors from a disc or disc image. -pub trait SectorReader: Send { - /// Read `count` sectors starting at `lba` into `buf`. - /// `buf` must be at least `count * 2048` bytes. - /// `recovery`: true = full retry/reset loop (ripping), false = single attempt (verify). - /// File-backed readers ignore the flag. - fn read_sectors( - &mut self, - lba: u32, - count: u16, - buf: &mut [u8], - recovery: bool, - ) -> Result; - - /// Total capacity in sectors, if known. - fn capacity(&self) -> u32 { - 0 - } - - fn set_speed(&mut self, _kbs: u16) {} -} - -/// SectorReader backed by a file (ISO image). -/// Seeks to lba * 2048, reads count * 2048 bytes. -pub struct FileSectorReader { - file: std::io::BufReader, - capacity: u32, -} - -impl FileSectorReader { - pub fn open(path: &str) -> std::io::Result { - let file = std::fs::File::open(path)?; - let len = file.metadata()?.len(); - let sectors = len / 2048; - if sectors > u32::MAX as u64 { - // ~8 TB hard cap (u32::MAX × 2048 bytes). Path lives in the - // typed Error variant — no English in the message. - return Err(crate::error::Error::IsoTooLarge { - path: path.to_string(), - } - .into()); - } - let capacity = sectors as u32; - Ok(Self { - file: std::io::BufReader::with_capacity(4 * 1024 * 1024, file), - capacity, - }) - } -} - -impl SectorReader for FileSectorReader { - fn read_sectors( - &mut self, - lba: u32, - count: u16, - buf: &mut [u8], - _recovery: bool, - ) -> Result { - use std::io::{Read, Seek, SeekFrom}; - let offset = lba as u64 * 2048; - let bytes = count as usize * 2048; - self.file - .seek(SeekFrom::Start(offset)) - .map_err(|e| crate::error::Error::IoError { source: e })?; - self.file - .read_exact(&mut buf[..bytes]) - .map_err(|e| crate::error::Error::IoError { source: e })?; - Ok(bytes) - } - - fn capacity(&self) -> u32 { - self.capacity - } -} diff --git a/src/sector/decrypting.rs b/src/sector/decrypting.rs new file mode 100644 index 0000000..3410b71 --- /dev/null +++ b/src/sector/decrypting.rs @@ -0,0 +1,190 @@ +//! `DecryptingSectorSource` — wrap any [`SectorSource`] to apply +//! AACS / CSS in-place decryption on every read. +//! +//! This is the 0.18 single-source-of-truth for decrypt-on-read. The +//! actual cipher code lives in [`crate::aacs`] and [`crate::css`]; +//! we just call the existing [`crate::decrypt::decrypt_sectors`] +//! helper that already drives both of them. In follow-up commits +//! `sweep_pipeline` and `DiscStream` migrate onto this decorator +//! and delete their duplicate decrypt call sites. +//! +//! Composition: `Drive` → `DecryptingSectorSource` → caller sees +//! plaintext. For `DecryptKeys::None` discs the decorator is a +//! pass-through, so callers can wire it unconditionally and keep +//! their pipeline shape uniform regardless of encryption state. + +use crate::decrypt::{DecryptKeys, decrypt_sectors}; +use crate::error::Result; + +use super::SectorSource; + +/// Decorator: read from `inner`, then run the configured +/// AACS / CSS decrypt over the bytes that landed in `buf`. +/// +/// `unit_key_idx` selects the AACS unit key for the disc (0 for +/// the vast majority of titles; the rare multi-CPS-unit discs pick +/// the index that covers the title being read). For +/// [`DecryptKeys::None`] and [`DecryptKeys::Css`] the index is +/// ignored. +pub struct DecryptingSectorSource { + inner: S, + keys: DecryptKeys, + unit_key_idx: usize, +} + +impl DecryptingSectorSource { + /// Wrap `inner` with the given keys. The default unit-key + /// index is 0; use [`with_unit_key_idx`] for the multi-CPS-unit + /// case. + /// + /// [`with_unit_key_idx`]: Self::with_unit_key_idx + pub fn new(inner: S, keys: DecryptKeys) -> Self { + Self { + inner, + keys, + unit_key_idx: 0, + } + } + + /// Override the AACS unit-key index. Only meaningful for + /// [`DecryptKeys::Aacs`]; other variants ignore it. + pub fn with_unit_key_idx(mut self, idx: usize) -> Self { + self.unit_key_idx = idx; + self + } + + /// Borrow the inner source. Useful for tests and for adapters + /// that want to introspect the underlying drive / file without + /// unwrapping the decorator. + pub fn inner(&self) -> &S { + &self.inner + } + + /// Mutable borrow of the inner source. + pub fn inner_mut(&mut self) -> &mut S { + &mut self.inner + } + + /// Consume the decorator and return the underlying source. + pub fn into_inner(self) -> S { + self.inner + } +} + +impl SectorSource for DecryptingSectorSource { + fn capacity_sectors(&self) -> u32 { + self.inner.capacity_sectors() + } + + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + recovery: bool, + ) -> Result { + let n = self.inner.read_sectors(lba, count, buf, recovery)?; + // Reuse the existing crate-wide decrypt entry point — same + // path the 0.17 sweep_pipeline and DiscStream call, so we + // inherit their AACS / CSS / None semantics verbatim. The + // helper is a no-op for DecryptKeys::None. + decrypt_sectors(&mut buf[..n], &self.keys, self.unit_key_idx)?; + Ok(n) + } + + fn set_speed(&mut self, kbs: u16) { + self.inner.set_speed(kbs) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::error::Result; + + /// Synthetic SectorSource that yields a deterministic byte + /// pattern keyed by LBA. Used to verify the decorator's + /// pass-through behaviour for `DecryptKeys::None`. + struct PatternedSource { + capacity: u32, + } + + impl PatternedSource { + fn fill(lba: u32, count: u16, buf: &mut [u8]) { + let bytes = count as usize * 2048; + for (i, slot) in buf[..bytes].iter_mut().enumerate() { + let abs = lba as u64 * 2048 + i as u64; + *slot = ((abs.wrapping_mul(2654435761) >> 16) & 0xff) as u8; + } + } + } + + impl SectorSource for PatternedSource { + fn capacity_sectors(&self) -> u32 { + self.capacity + } + + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + _recovery: bool, + ) -> Result { + Self::fill(lba, count, buf); + Ok(count as usize * 2048) + } + } + + #[test] + fn passthrough_with_no_keys() { + let src = PatternedSource { capacity: 16 }; + let mut wrapped = DecryptingSectorSource::new(src, DecryptKeys::None); + + // capacity_sectors delegates. + assert_eq!(wrapped.capacity_sectors(), 16); + + let mut got = vec![0u8; 4 * 2048]; + let n = wrapped.read_sectors(3, 4, &mut got, false).unwrap(); + assert_eq!(n, 4 * 2048); + + let mut expected = vec![0u8; 4 * 2048]; + PatternedSource::fill(3, 4, &mut expected); + assert_eq!(got, expected); + } + + #[test] + fn passthrough_set_speed_delegates() { + struct SpeedRecorder { + last: Option, + } + impl SectorSource for SpeedRecorder { + fn capacity_sectors(&self) -> u32 { + 0 + } + fn read_sectors( + &mut self, + _lba: u32, + _count: u16, + _buf: &mut [u8], + _recovery: bool, + ) -> Result { + Ok(0) + } + fn set_speed(&mut self, kbs: u16) { + self.last = Some(kbs); + } + } + + let mut wrapped = + DecryptingSectorSource::new(SpeedRecorder { last: None }, DecryptKeys::None); + wrapped.set_speed(7200); + assert_eq!(wrapped.inner().last, Some(7200)); + } + + // TODO: AACS round-trip test — needs a fixture-encrypted unit + // (6144-byte aligned) plus the matching unit key. The cipher + // path itself is exercised by `crate::aacs` unit tests; here + // we only assert the decorator wires the existing helper, not + // that AES-128 is correct. +} diff --git a/src/sector/file.rs b/src/sector/file.rs new file mode 100644 index 0000000..9e95c51 --- /dev/null +++ b/src/sector/file.rs @@ -0,0 +1,245 @@ +//! File-backed sector I/O — read and write 2048-byte sectors against +//! an ISO image on disk. +//! +//! [`FileSectorSource`] is the read side (open-only). [`FileSectorSink`] +//! is the write side (create or open-rw); writes go through +//! [`crate::io::Writer`] so big sequential ISO writes share the +//! same bounded-cache writeback pipeline used by sweep / patch / +//! mux. `Writer` is the 0.17 name; the 0.18 redesign renames it +//! to `WritebackFile` in a separate slice — this file deliberately +//! imports through the `crate::io::Writer` path so the rename can +//! be applied independently. + +use std::fs::{File, OpenOptions}; +use std::io::{BufReader, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +use crate::error::{Error, Result}; + +use super::{SectorReader, SectorSink}; + +/// SectorSource backed by a file (ISO image). +/// +/// Seeks to `lba * 2048`, reads `count * 2048` bytes per call. The +/// underlying file is wrapped in a 4 MiB `BufReader` so adjacent +/// small reads coalesce into single syscalls. +pub struct FileSectorSource { + file: BufReader, + capacity: u32, +} + +impl FileSectorSource { + /// Open an existing ISO file for reading. Capacity is derived + /// from `metadata().len() / 2048`. Returns + /// [`Error::IsoTooLarge`] if the file would exceed the 32-bit + /// LBA address space (~8 TB). + pub fn open(path: &str) -> std::io::Result { + let file = File::open(path)?; + let len = file.metadata()?.len(); + let sectors = len / 2048; + if sectors > u32::MAX as u64 { + return Err(Error::IsoTooLarge { + path: path.to_string(), + } + .into()); + } + let capacity = sectors as u32; + Ok(Self { + file: BufReader::with_capacity(4 * 1024 * 1024, file), + capacity, + }) + } +} + +// Implement the legacy `SectorReader` trait. The blanket impl in +// `super` produces the `SectorSource` impl automatically — no need +// to write both, and writing both would conflict. This keeps the +// 0.17 method-resolution path intact (callers with `SectorReader` +// in scope can still write `fsr.read_sectors(..)` against a +// `FileSectorSource`). +impl SectorReader for FileSectorSource { + fn capacity(&self) -> u32 { + self.capacity + } + + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + _recovery: bool, + ) -> Result { + let offset = lba as u64 * 2048; + let bytes = count as usize * 2048; + self.file + .seek(SeekFrom::Start(offset)) + .map_err(|e| Error::IoError { source: e })?; + self.file + .read_exact(&mut buf[..bytes]) + .map_err(|e| Error::IoError { source: e })?; + Ok(bytes) + } +} + +/// SectorSink backed by a file (ISO image). +/// +/// Writes go through [`crate::io::Writer`], which on Linux drives +/// continuous `sync_file_range` + `posix_fadvise(DONTNEED)` to keep +/// the kernel dirty page cache bounded during multi-GB sequential +/// writes. macOS / Windows fall through to a no-op pipeline. +/// +/// `finish` runs `sync_all` before dropping the underlying file. +pub struct FileSectorSink { + inner: crate::io::Writer, +} + +impl FileSectorSink { + /// Create a new ISO file at `path`, truncating any existing + /// file. The file is opened read-write so the same handle can + /// later be reused for verification reads if needed (sweep + /// doesn't, but it costs nothing here). + pub fn create(path: &Path) -> std::io::Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(path)?; + Ok(Self { + inner: crate::io::Writer::new(file)?, + }) + } + + /// Open an existing ISO file for in-place updates (e.g. patch + /// pass writing recovered sectors over zero-filled holes). + /// Does not truncate. + pub fn open(path: &Path) -> std::io::Result { + let file = OpenOptions::new().read(true).write(true).open(path)?; + Ok(Self { + inner: crate::io::Writer::new(file)?, + }) + } +} + +impl SectorSink for FileSectorSink { + fn write_sectors(&mut self, lba: u32, buf: &[u8]) -> Result<()> { + debug_assert!( + buf.len() % 2048 == 0, + "FileSectorSink::write_sectors: buf len {} not a multiple of 2048", + buf.len() + ); + let offset = lba as u64 * 2048; + self.inner + .seek(SeekFrom::Start(offset)) + .map_err(|e| Error::IoError { source: e })?; + self.inner + .write_all(buf) + .map_err(|e| Error::IoError { source: e })?; + Ok(()) + } + + fn finish(mut self: Box) -> Result<()> { + self.inner + .sync_all() + .map_err(|e| Error::IoError { source: e })?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + // Bring the 0.18 trait into scope (not super::*: the super + // module also re-exports the legacy `SectorReader`, and + // having both `SectorReader::read_sectors` and + // `SectorSource::read_sectors` visible would force every + // call site to disambiguate). External consumers see the + // same surface this test exercises. + use super::{FileSectorSink, FileSectorSource}; + use crate::sector::{SectorSink, SectorSource}; + use tempfile::tempdir; + + #[test] + fn round_trip_single_sector() { + let dir = tempdir().unwrap(); + let path = dir.path().join("rt.iso"); + + let mut sink = FileSectorSink::create(&path).unwrap(); + // Pre-extend the file to 4 sectors of zeros so we can write + // sector 2 in place. Easiest way: write zeros first. + let zeros = [0u8; 4 * 2048]; + sink.write_sectors(0, &zeros).unwrap(); + + let mut payload = [0u8; 2048]; + for (i, b) in payload.iter_mut().enumerate() { + *b = (i as u8).wrapping_mul(17); + } + sink.write_sectors(2, &payload).unwrap(); + Box::new(sink).finish().unwrap(); + + let mut src = FileSectorSource::open(path.to_str().unwrap()).unwrap(); + assert_eq!(src.capacity_sectors(), 4); + + let mut got = [0u8; 2048]; + let n = src.read_sectors(2, 1, &mut got, false).unwrap(); + assert_eq!(n, 2048); + assert_eq!(got, payload); + + // Sectors 0,1,3 still zero. + let mut z = [0xffu8; 2048]; + src.read_sectors(0, 1, &mut z, false).unwrap(); + assert!(z.iter().all(|b| *b == 0)); + } + + #[test] + fn round_trip_multi_sector() { + let dir = tempdir().unwrap(); + let path = dir.path().join("multi.iso"); + + let mut sink = FileSectorSink::create(&path).unwrap(); + let mut payload = vec![0u8; 8 * 2048]; + for (i, b) in payload.iter_mut().enumerate() { + *b = ((i * 31) ^ (i >> 7)) as u8; + } + sink.write_sectors(0, &payload).unwrap(); + Box::new(sink).finish().unwrap(); + + let mut src = FileSectorSource::open(path.to_str().unwrap()).unwrap(); + assert_eq!(src.capacity_sectors(), 8); + + let mut got = vec![0u8; 8 * 2048]; + let n = src.read_sectors(0, 8, &mut got, false).unwrap(); + assert_eq!(n, 8 * 2048); + assert_eq!(got, payload); + } + + #[test] + fn open_existing_does_not_truncate() { + let dir = tempdir().unwrap(); + let path = dir.path().join("open.iso"); + + // Create with 4 sectors of pattern A. + let mut sink = FileSectorSink::create(&path).unwrap(); + let pat_a = [0xaau8; 4 * 2048]; + sink.write_sectors(0, &pat_a).unwrap(); + Box::new(sink).finish().unwrap(); + + // Reopen and overwrite sector 1 only. + let mut sink = FileSectorSink::open(&path).unwrap(); + let pat_b = [0xbbu8; 2048]; + sink.write_sectors(1, &pat_b).unwrap(); + Box::new(sink).finish().unwrap(); + + let mut src = FileSectorSource::open(path.to_str().unwrap()).unwrap(); + assert_eq!(src.capacity_sectors(), 4); + let mut got = [0u8; 2048]; + + src.read_sectors(0, 1, &mut got, false).unwrap(); + assert_eq!(got, [0xaau8; 2048]); + + src.read_sectors(1, 1, &mut got, false).unwrap(); + assert_eq!(got, [0xbbu8; 2048]); + + src.read_sectors(2, 1, &mut got, false).unwrap(); + assert_eq!(got, [0xaau8; 2048]); + } +} diff --git a/src/sector/mod.rs b/src/sector/mod.rs new file mode 100644 index 0000000..720a701 --- /dev/null +++ b/src/sector/mod.rs @@ -0,0 +1,154 @@ +//! Sector-level I/O traits. +//! +//! 0.18 splits the unidirectional read trait from a write trait at +//! the sector layer, so the type system catches "wrong direction" +//! mistakes at compile time instead of runtime. See +//! `(internal)/memory/0_18_redesign.md`. +//! +//! - [`SectorSource`] reads 2048-byte sectors. Implemented by +//! `Drive` (via the legacy [`SectorReader`] alias) and +//! [`FileSectorSource`] (ISO-backed). +//! - [`SectorSink`] writes 2048-byte sectors. Implemented by +//! [`FileSectorSink`] (ISO-backed) and, in later commits, by +//! sweep/patch consumer adapters. +//! - [`DecryptingSectorSource`] is a decorator that wraps any +//! `SectorSource` and applies the existing AACS / CSS in-place +//! decrypt to plaintext-out. +//! +//! [`SectorReader`] is the 0.17 read trait. It stays on through +//! the 0.18 migration window so existing call sites +//! (`Drive`, `IsoSectorReader`, `BufferedSectorReader`, +//! `DiscStream`, `verify`) compile unchanged. A blanket impl +//! forwards every `SectorReader` impl to `SectorSource`, so new +//! code should target `SectorSource` / `SectorSink` directly. The +//! formal `#[deprecated]` attribute lands once the internal +//! callers have migrated; see the comment on `SectorReader` for +//! why this commit holds it back. + +pub mod decrypting; +pub mod file; + +use crate::error::Result; + +/// Read 2048-byte sectors from a disc, image, or composed source. +/// +/// Direction-typed: a `SectorSource` cannot be written to. Wrap the +/// inner source in [`DecryptingSectorSource`] to get plaintext +/// sectors out of an encrypted disc. +pub trait SectorSource: Send { + /// Total capacity in sectors, if known. Returns 0 when unknown + /// (e.g. live drives that haven't completed `READ CAPACITY` yet). + fn capacity_sectors(&self) -> u32; + + /// Read `count` sectors starting at `lba` into `buf`. + /// `buf` must be at least `count * 2048` bytes. + /// `recovery`: true = full retry/reset loop (ripping), + /// false = single attempt (verify). File-backed sources ignore + /// the flag. + /// + /// Returns the number of bytes written into `buf` on success. + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + recovery: bool, + ) -> Result; + + /// Optional speed control for sources that map to a physical + /// drive. No-op for everything else. + fn set_speed(&mut self, _kbs: u16) {} +} + +/// Write 2048-byte sectors to a disc image or composed sink. +/// +/// Direction-typed: a `SectorSink` cannot be read from. The +/// terminal [`finish`] takes `Box` so it can run on `dyn +/// SectorSink` and consume the sink (`fsync` + close). +/// +/// [`finish`]: SectorSink::finish +pub trait SectorSink: Send { + /// Write the sectors in `buf` starting at `lba`. `buf.len()` + /// must be a multiple of 2048; the implementation seeks to + /// `lba * 2048` before writing. + fn write_sectors(&mut self, lba: u32, buf: &[u8]) -> Result<()>; + + /// Flush, fsync, and close. Consumes the sink. Always called + /// last; subsequent operations are not defined. + fn finish(self: Box) -> Result<()>; +} + +/// 0.17 read trait. Slated for removal once internal call sites +/// migrate to [`SectorSource`] in follow-up commits; until then +/// it remains the trait that `Drive`, `IsoSectorReader`, +/// `BufferedSectorReader`, and existing `&mut dyn SectorReader` +/// signatures use unchanged. +/// +/// New code should implement [`SectorSource`] directly. The +/// blanket impl below makes any `SectorReader` automatically +/// usable wherever a `SectorSource` is expected, so a one-way +/// migration off `SectorReader` is possible per-callsite without +/// touching the impls. +// +// NOTE: not marked `#[deprecated]` in this commit — `cargo clippy +// -- -D warnings` (the CI gauntlet) treats deprecation as an +// error, and the existing `Drive` / `udf::BufferedSectorReader` / +// `mux::DiscStream` / `verify` call sites all go through this +// trait. The deprecation attribute lands together with the +// migration commits that move those call sites to +// `SectorSource`. The behavioural contract — "this trait is +// going away in 0.18" — is documented above and tracked in +// `(internal)/memory/0_18_redesign.md`. +pub trait SectorReader: Send { + /// Read `count` sectors starting at `lba` into `buf`. + /// See [`SectorSource::read_sectors`] for semantics. + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + recovery: bool, + ) -> Result; + + /// Total capacity in sectors, if known. + fn capacity(&self) -> u32 { + 0 + } + + fn set_speed(&mut self, _kbs: u16) {} +} + +// Blanket impl: anything implementing the legacy `SectorReader` +// trait automatically satisfies `SectorSource`. This is what keeps +// existing impls (`Drive`, `IsoSectorReader`, `BufferedSectorReader`, +// etc.) compiling without source changes during the migration. The +// reverse direction (impl SectorReader for SectorSource) is +// intentionally NOT provided — new code targets the new trait. +impl SectorSource for T { + fn capacity_sectors(&self) -> u32 { + ::capacity(self) + } + + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + recovery: bool, + ) -> Result { + ::read_sectors(self, lba, count, buf, recovery) + } + + fn set_speed(&mut self, kbs: u16) { + ::set_speed(self, kbs) + } +} + +pub use decrypting::DecryptingSectorSource; +pub use file::{FileSectorSink, FileSectorSource}; + +// Backwards-compat alias for the public API. `FileSectorReader` is +// the 0.17 name; new code uses `FileSectorSource`. Both point at +// the same type. The `#[deprecated]` attribute lands together with +// the migration commits that retire the alias from internal uses. +pub type FileSectorReader = FileSectorSource;