1.6.0: remove recovery strategy (moved to freemkv-engine) + trim dead surface
The sweep/patch recovery strategy, mapfile, retry-decision state machine,
section-recover, and damage classification move out of libfreemkv into the
new freemkv-engine crate. libfreemkv keeps the raw single-shot read and
SCSI-fact translation (SenseFamily stays in scsi).
- Delete disc/{sweep,patch,mapfile,read_error,section_recover}.rs, the
Disc::copy/sweep/patch methods, the Copy/Sweep/Patch option+result types,
classify_damage/DamageSeverity, progress_snapshot_from_mapfile, and the
three recovery integration tests.
- Trim public surface the recovery deletion orphaned: delete the dead
READ_PIPELINE_DEPTH const, the write-side SectorSink/FileSectorSink (no
consumer), and the DriveSpeed enum (its one live use — set max drive
speed — becomes Drive::SPEED_MAX_KBPS). Make mapfile_path_for,
decrypt_sectors_mapped pub(crate); gate NoopEvents to test.
- Version 1.6.0.
This commit is contained in:
@@ -1,180 +0,0 @@
|
||||
//! File-backed sector sink — write 2048-byte sectors to an ISO image
|
||||
//! on disk.
|
||||
//!
|
||||
//! The read-side counterpart ([`crate::io::file_sector_source::FileSectorSource`])
|
||||
//! lives under `io/` because its internals (read-ahead buffer, per-OS
|
||||
//! `fadvise`/`F_RDADVISE` hints) are I/O infrastructure rather than
|
||||
//! sector-trait business logic. Both types remain re-exported at
|
||||
//! [`crate::sector`] for ergonomic imports.
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
use super::SectorSink;
|
||||
|
||||
/// SectorSink backed by a file (ISO image).
|
||||
///
|
||||
/// Writes go through [`crate::io::WritebackFile`], 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::WritebackFile,
|
||||
}
|
||||
|
||||
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::WritebackFile::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::WritebackFile::new(file)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SectorSink for FileSectorSink {
|
||||
fn write_sectors(&mut self, lba: u32, buf: &[u8]) -> Result<()> {
|
||||
// SectorSink's contract requires a 2048-multiple buffer. Enforce
|
||||
// it in all build modes (a `debug_assert!` is a no-op in release):
|
||||
// a misaligned buffer would `write_all` partial bytes at
|
||||
// lba*2048 and silently corrupt the ISO. Current in-tree callers
|
||||
// always pass aligned buffers; this guards the public trait
|
||||
// contract against any (including future external) caller.
|
||||
if buf.len() % 2048 != 0 {
|
||||
return Err(Error::IoError {
|
||||
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
|
||||
});
|
||||
}
|
||||
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 {
|
||||
use super::FileSectorSink;
|
||||
use crate::io::file_sector_source::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).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).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).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]);
|
||||
}
|
||||
}
|
||||
+2
-27
@@ -1,20 +1,14 @@
|
||||
//! Sector-level I/O traits.
|
||||
//! Sector-level read I/O traits.
|
||||
//!
|
||||
//! The sector layer is direction-typed: [`SectorSource`] reads
|
||||
//! 2048-byte sectors, [`SectorSink`] writes them. Concrete impls
|
||||
//! never do both — physical drives are read-only, file-backed
|
||||
//! ISO images are opened for read OR write at construction time.
|
||||
//! [`SectorSource`] reads 2048-byte sectors from a disc.
|
||||
//!
|
||||
//! - [`SectorSource`] is implemented by `Drive` (hardware) and
|
||||
//! [`FileSectorSource`] (file-backed).
|
||||
//! - [`SectorSink`] is implemented by [`FileSectorSink`]
|
||||
//! (ISO-backed).
|
||||
//! - [`DecryptingSectorSource`] is a decorator that wraps any
|
||||
//! `SectorSource` and applies AACS / CSS in-place decrypt to
|
||||
//! yield plaintext sectors.
|
||||
|
||||
pub mod decrypting;
|
||||
pub mod file;
|
||||
pub mod prefetched;
|
||||
|
||||
use crate::error::Result;
|
||||
@@ -169,27 +163,8 @@ impl SectorSource for &mut (dyn SectorSource + '_) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write 2048-byte sectors to a disc image or composed sink.
|
||||
///
|
||||
/// 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 as u64 * 2048` before writing (the `u64` cast is required —
|
||||
/// a bare `u32` `lba * 2048` wraps past ~4 GB on UHD-scale images).
|
||||
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<()>;
|
||||
}
|
||||
|
||||
pub use crate::io::file_sector_source::FileSectorSource;
|
||||
pub use decrypting::{DecryptingSectorSource, KeyFetch, KeyFetchFn};
|
||||
pub use file::FileSectorSink;
|
||||
pub use prefetched::PrefetchedSectorSource;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user