0.18: SectorSource/SectorSink trait split + DecryptingSectorSource decorator
This commit is contained in:
+12
-4
@@ -180,10 +180,18 @@ pub use mux::{InputOptions, StreamUrl, input, output, parse_url};
|
|||||||
// ─── Lower-level surfaces ───────────────────────────────────────────────────
|
// ─── Lower-level surfaces ───────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// `ScsiTransport` is the platform-abstraction trait Drive uses; expose for
|
// `ScsiTransport` is the platform-abstraction trait Drive uses; expose for
|
||||||
// out-of-tree platform backends. `SectorReader` lets callers feed any byte
|
// out-of-tree platform backends. `SectorSource` / `SectorSink` are the 0.18
|
||||||
// source (test harness, network image, SMB share) into the disc scan
|
// direction-typed read/write traits; `FileSectorSource` and `FileSectorSink`
|
||||||
// pipeline; `FileSectorReader` is the standard ISO-on-disk implementation.
|
// 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 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 speed::DriveSpeed;
|
||||||
pub use udf::{UdfFs, read_filesystem};
|
pub use udf::{UdfFs, read_filesystem};
|
||||||
|
|||||||
@@ -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<usize>;
|
|
||||||
|
|
||||||
/// 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<std::fs::File>,
|
|
||||||
capacity: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileSectorReader {
|
|
||||||
pub fn open(path: &str) -> std::io::Result<Self> {
|
|
||||||
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<usize> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<S: SectorSource> {
|
||||||
|
inner: S,
|
||||||
|
keys: DecryptKeys,
|
||||||
|
unit_key_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SectorSource> DecryptingSectorSource<S> {
|
||||||
|
/// 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<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
||||||
|
fn capacity_sectors(&self) -> u32 {
|
||||||
|
self.inner.capacity_sectors()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_sectors(
|
||||||
|
&mut self,
|
||||||
|
lba: u32,
|
||||||
|
count: u16,
|
||||||
|
buf: &mut [u8],
|
||||||
|
recovery: bool,
|
||||||
|
) -> Result<usize> {
|
||||||
|
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<usize> {
|
||||||
|
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<u16>,
|
||||||
|
}
|
||||||
|
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<usize> {
|
||||||
|
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.
|
||||||
|
}
|
||||||
@@ -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<File>,
|
||||||
|
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<Self> {
|
||||||
|
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<usize> {
|
||||||
|
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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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<Self>) -> 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<usize>;
|
||||||
|
|
||||||
|
/// 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<Self>` 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<Self>) -> 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<usize>;
|
||||||
|
|
||||||
|
/// 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<T: SectorReader + ?Sized> SectorSource for T {
|
||||||
|
fn capacity_sectors(&self) -> u32 {
|
||||||
|
<T as SectorReader>::capacity(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_sectors(
|
||||||
|
&mut self,
|
||||||
|
lba: u32,
|
||||||
|
count: u16,
|
||||||
|
buf: &mut [u8],
|
||||||
|
recovery: bool,
|
||||||
|
) -> Result<usize> {
|
||||||
|
<T as SectorReader>::read_sectors(self, lba, count, buf, recovery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_speed(&mut self, kbs: u16) {
|
||||||
|
<T as SectorReader>::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;
|
||||||
Reference in New Issue
Block a user