diff --git a/.gitignore b/.gitignore index ea8c4bf..b60de5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/target +**/target diff --git a/Cargo.toml b/Cargo.toml index 04a3a89..60ffb36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,29 @@ -[package] -name = "kramer" -version = "0.1.0" +[workspace] +resolver = "3" +members = ["crates/*"] +default-members = ["crates/kramer"] + +[workspace.package] +version = "0.3.0" edition = "2024" authors = ["Olivia Brooks"] repository = "https://gitea.cutieguwu.ca/cutieguwu/kramer" license = "MIT" publish = false -[dependencies] -num-traits = "0.2.19" -ratatui = "0.30" -ron = ">=0.8, <0.13" -#rust-i18n = "3.1.3" +[workspace.dependencies] +# +# Workspace member crates +# +libdvdcss = { path = "crates/libdvdcss" } -[dependencies.anyhow] -version = "1.0" -features = ["backtrace"] - -[dependencies.clap] -version = "4.5" -features = ["derive"] - -[dependencies.serde] -version = "1.0" -features = ["derive"] +# +# External crates +# +#anyhow = { version = "1.0", features = ["backtrace"] } +#clap = { version = "4.5", features = ["derive"] } +derive_more = { version = "2.1", features = ["display", "from"] } +#serde = { version = "1.0", features = ["derive"] } # Yes. For one constant, this library is required. # Technically, this did a bit more in early testing when I messed about @@ -31,5 +31,8 @@ features = ["derive"] # # And yes, I spent time tracking down the first release with that constant. # v0.2.25 is almost 9 years old as of writing this comment. -[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] -libc = "~0.2.25" +#[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +#libc = "~0.2.25" + +[profile.dev] +incremental = true diff --git a/README.md b/README.md index d9509c4..bcf7854 100644 --- a/README.md +++ b/README.md @@ -7,64 +7,65 @@ will continue to be referred to as kramer. This is still in very early development, so expect old maps to no longer work. -## Plans +## Where is it at? -### Core +At this point, `kramer` is perhaps more of a solo research project in the sense +that I am in way over my head. I lack the necessary experience to take on such a +project, but I am more than stubborn enough to keep restarting, making little +strides further than previous attempts. -- [x] Mapping - - [x] Record the state of disc regions. - - [x] Recover from saved map. - - [x] Backup old map before truncating for new. -- [ ] Recovery - - [x] Initial / Patchworking - Technically there is an outstanding issue with sleepy firmware here, - but beside that this technically works. - - [ ] Isolate - - [ ] Scraping -- [ ] CLI - - [x] Arguments - - [ ] Recovery progress - - [ ] Recovery stats -- [ ] Documentation, eugh. +As such, you have been warned. -### Extra +## What have I managed to achieve so far? -- [ ] i18n - - [ ] English - - [ ] French -- [ ] TUI (akin to `ddrescueview`) - - [ ] Visual status map - - [ ] Recovery properties - - [ ] Recovery progress - - [ ] Recovery stats +So far, by blindly bumbling my ??? through the Sharran darkness, I have managed: -## Recovery Strategy +### `DIRECT_IO` reading in Rust on platforms which support it -### Initial Pass / Patchworking +Effectively bypassing the kernel buffer and other safeties offered by the +kernel which inhibit reading past IO failure. -Tries to read clusters of `max_buffer_size`, marking clusters with errors with -an increasing `level`. +### Tracking of recovery progress by mapping disc regions -This works by halving the length of the read buffer until one of two -conditions is met: +This includes working map defragmentation. I did *not* enjoy figuring out all +13 cases for that. -1. `max_buffer_size` has been divided by `max_buffer_subdivision` - (like a maximum recursion depth). - Effectively, it will keep running `max_buffer_size / isolation_depth; - isolation_depth++;` on each pass until `isolation_depth == - max_buffer_subdivision` -2. `buffer_size <= min_buffer_size` -3. `buffer_size <= sector_size` +This code and its associated methods need to be reviewed. Current iteration is +a *really* hack and unreadable solution. -### Isolate +### Possibly wrapping `libdvdcss` for SCSI bus encryption -This is where we reach brute forcing territory. `ddrescue` refers to this as -trimming. `kramer` implements the same technique. However, thanks to the -patchworking pass, this sector-at-a-time reading can be minimized, hopefully -reducing wear and overall recovery time on drives with a very short spin-down -delay. +This is yet untested, but I have written a wrapper with the help of `bindgen`. -### Scraping (Stage::BruteForceAndDesperation) +I really have two options here: -This is the pure brute force, sector-at-a-time read. This has identical -behaviour to `ddrescue`'s scraping phase. +1. I don't do any detection of CSS on a disc and risk forcing a system to have + a hard shutdown due to the specially allocated memory buffer required for + `DIRECT_IO`. Once the drive stops responding to SCSI commands following + enough attempted reads of scrambled sectors, which *could* be a false + positive by the drive (the encryption flag in stored within MPEG-2 PACK + headers, and could be bit flipped), the kernel will refuse to deallocate the + memory on behalf of the frozen \[synchronous\] program. +2. I support *only* bus decryption through *optional* `libdvdcss` integration. + False positives are no longer a problem, and the drive will maintain contact + with the system, as all reads will be authenticated. + +## Next Steps + +Mostly in order of priority: + +- Total refactor for a plugin-based architecture. +- Lightweight data scraping plugin. Simple single-pass, for early testing + purposes. +- Tagging-based mapping instead of just region and recovery stage. + - With support for Ser/De to object notation. +- Optional CSS bus encryption support via `libdvdcss`. +- Async operation +- Full-featured data scraping plugin. +- Basic CLI +- ISO9660 and UDF structure analysis plugin. +- ISO9660 and UDF structure repair plugin. +- MPEG2 header structure analysis plugin. +- MPEG2 header structure repair plugin. +- TUI +- i18n diff --git a/crates/kramer/Cargo.toml b/crates/kramer/Cargo.toml new file mode 100644 index 0000000..8bd124b --- /dev/null +++ b/crates/kramer/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name.workspace = true +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] diff --git a/crates/kramer/src/main.rs b/crates/kramer/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/crates/kramer/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index e9e3794..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::path::PathBuf; -use std::sync::LazyLock; - -use clap::{ArgAction, Parser}; - -pub static CONFIG: LazyLock = LazyLock::new(|| Args::parse()); - -#[derive(Parser, Debug, Clone)] -#[clap(author, version, about)] -pub struct Args { - /// Path to source file or block device - #[arg(short, long, value_hint = clap::ValueHint::DirPath)] - pub input: PathBuf, - - /// Path to output file. Defaults to {input}.iso - #[arg(short, long, value_hint = clap::ValueHint::DirPath)] - pub output: Option, - - /// Path to rescue map. Defaults to {input}.map - #[arg(short, long, value_hint = clap::ValueHint::DirPath)] - pub map: Option, - - /// Max number of consecutive sectors to test as a group - #[arg(short, long, default_value_t = crate::FB_CLUSTER_LEN)] - pub cluster_length: usize, - - /// Number of brute force read passes - #[arg(short, long, default_value_t = 2)] - pub brute_passes: usize, - - /// Sector size - #[arg(short, long, default_value_t = crate::FB_SECTOR_SIZE)] - pub sector_size: usize, - - // !!! ArgAction behaviour is backwards to what you want to think !!! - // ArgAction::SetFalse sets default value to true, - // ArgAction::SetTrue sets default value to false. - // - // This is because ArgAction refers to the action that should happen *when* the flag is - // provided. - // I.e. a flag to disable something should take action to set the value as false. - // - /// Upon encountering a read error, reopen the source file before continuing. - #[arg(short, long, action = ArgAction::SetTrue)] - pub reopen_on_error: bool, - - /// Use O_DIRECT to bypass kernel buffer when reading. - /// It is very unlikely that you will want to disable this. - // - // BSD seems to support O_DIRECT, but MacOS for certain does not. - #[cfg(all(unix, not(target_os = "macos")))] - #[arg(short = 'd', long = "no-direct", action = ArgAction::SetFalse)] - pub direct_io: bool, - - /// Disable the TUI in favour of a classic CLI experience. - #[arg(short = 't', long = "no-tui", action = ArgAction::SetFalse)] - pub tui: bool, -} diff --git a/src/io.rs b/src/io.rs deleted file mode 100644 index d88b29e..0000000 --- a/src/io.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::fs::{File, OpenOptions}; -use std::io::{self, Seek, SeekFrom}; -use std::ops::Index; -use std::path::Path; - -use crate::cli::CONFIG; - -use anyhow::{Context, anyhow}; - -/// Get length of data stream. -/// Physical length of data stream in bytes -/// (multiple of sector_size, rather than actual). -/// -/// This will attempt to return the stream to its current read position. -pub fn get_stream_length(stream: &mut S) -> io::Result { - let pos = stream.stream_position()?; - let len = stream.seek(SeekFrom::End(0)); - - stream.seek(SeekFrom::Start(pos))?; - - len -} - -#[cfg(all(unix, not(target_os = "macos")))] -pub fn load_input() -> anyhow::Result { - use std::os::unix::fs::OpenOptionsExt; - - let mut options = OpenOptions::new(); - options.read(true); - - if CONFIG.direct_io { - options.custom_flags(libc::O_DIRECT); - } - - options - .open(&CONFIG.input) - .with_context(|| format!("Failed to open input file: {}", &CONFIG.input.display())) -} - -#[cfg(any(not(unix), target_os = "macos"))] -pub fn load_input() -> anyhow::Result { - OpenOptions::new() - .read(true) - .open(&CONFIG.input) - .with_context(|| format!("Failed to open input file: {}", &CONFIG.input.display())) -} - -pub fn load_output() -> anyhow::Result { - OpenOptions::new() - .read(true) - .write(true) - .create(true) - .open(crate::path::OUTPUT_PATH.clone()) - .with_context(|| { - format!( - "Failed to open/create output file at: {}", - crate::path::OUTPUT_PATH.display() - ) - }) -} - -pub fn load_map_read() -> std::io::Result { - OpenOptions::new() - .read(true) - .open(crate::path::MAP_PATH.clone()) -} - -pub fn load_map_write() -> anyhow::Result { - // Attempt to check if a map exists on the disk. - // If so, make a backup of it. - // - // This should be recoverable by just skipping over this error and logging a warning, - // but for now it will be an error condition. - if std::fs::exists(crate::path::MAP_PATH.clone()) - .context("Could not check if map exists in fs to make a backup.")? - { - backup(crate::path::MAP_PATH.clone())?; - } - - OpenOptions::new() - .write(true) - .create(true) - .truncate(true) // Wipe old map, in case we skip over backing up the old one. - .open(crate::path::MAP_PATH.clone()) - .with_context(|| { - format!( - "Failed to open map file at: {}", - crate::path::MAP_PATH.display() - ) - }) -} - -fn backup>(path: P) -> std::io::Result<()> { - std::fs::rename( - &path, - format!("{}.bak", path.as_ref().to_path_buf().display()), - ) -} - -#[derive(Debug)] -#[repr(C, align(512))] -pub struct DirectIOBuffer(pub [u8; crate::MAX_BUFFER_SIZE]); - -impl DirectIOBuffer { - pub fn new() -> Self { - Self::default() - } -} - -impl Default for DirectIOBuffer { - fn default() -> Self { - Self([crate::FB_NULL_VALUE; _]) - } -} - -impl From<[u8; crate::MAX_BUFFER_SIZE]> for DirectIOBuffer { - fn from(value: [u8; crate::MAX_BUFFER_SIZE]) -> Self { - Self(value) - } -} - -impl TryFrom<&[u8]> for DirectIOBuffer { - type Error = anyhow::Error; - - fn try_from(value: &[u8]) -> Result { - if value.len() > crate::MAX_BUFFER_SIZE { - return Err(anyhow!("Provided slice is larger than MAX_BUFFER_SIZE.")); - } - - Ok(Self(value.try_into()?)) - } -} - -impl AsRef<[u8]> for DirectIOBuffer { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl AsMut<[u8]> for DirectIOBuffer { - fn as_mut(&mut self) -> &mut [u8] { - &mut self.0 - } -} - -impl Index for DirectIOBuffer -where - Idx: std::slice::SliceIndex<[u8], Output = [u8]>, -{ - type Output = Idx::Output; - fn index(&self, index: Idx) -> &Self::Output { - &self.0.as_slice()[index] - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 5835c0a..0000000 --- a/src/main.rs +++ /dev/null @@ -1,55 +0,0 @@ -mod cli; -mod io; -mod mapping; -mod path; -mod recovery; -mod tui; - -use std::sync::mpsc; -use std::thread; - -use cli::CONFIG; -use recovery::Recover; -use tui::Tui; - -use anyhow; - -const FB_SECTOR_SIZE: usize = 2048; -const FB_CLUSTER_LEN: usize = 128; -const FB_NULL_VALUE: u8 = 0; - -const MAX_BUFFER_SIZE: usize = FB_SECTOR_SIZE * FB_CLUSTER_LEN; - -fn main() -> anyhow::Result<()> { - let mut recover_tool = Recover::new()?; - recover_tool.run()?; - - if CONFIG.tui { - run_tui()?; - } else { - run_cli(); - } - - Ok(()) -} - -fn run_cli() {} - -fn run_tui() -> std::io::Result<()> { - let mut tui = Tui::new(); - - // Enter Raw terminal mode. - let mut terminal = ratatui::init(); - - let (tx, rx) = mpsc::channel::(); - - let tx_input_fetcher = tx.clone(); - thread::spawn(move || tui::input_fetcher(tx_input_fetcher)); - - let tui_result = tui.run(&mut terminal, rx); - - // Exit Raw terminal mode. - ratatui::restore(); - - tui_result -} diff --git a/src/mapping/cluster.rs b/src/mapping/cluster.rs deleted file mode 100644 index 7d2c861..0000000 --- a/src/mapping/cluster.rs +++ /dev/null @@ -1,62 +0,0 @@ -use super::{Domain, Stage}; - -use serde::{Deserialize, Serialize}; - -/// A map for data stored in memory for processing and saving to disk. -// derived Ord impl *should* use self.domain.start to sort? Not sure. -// Use `sort_by_key()` to be safe. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct Cluster { - domain: Domain, - pub stage: Stage, -} - -impl Default for Cluster { - fn default() -> Self { - Cluster { - domain: Domain::from(0..1), - stage: Stage::default(), - } - } -} - -impl Cluster { - /// Breaks apart into a vec of clusters, - /// each of cluster_size, excepting last. - #[allow(dead_code)] - pub fn subdivide(&mut self, cluster_len: usize) -> Vec { - let domain_len = self.domain.len(); - let mut start = self.domain.start; - let mut clusters: Vec = vec![]; - - for _ in 0..(domain_len / cluster_len) { - clusters.push(Cluster { - domain: Domain::from(start..start + cluster_len), - stage: self.stage, - }); - - start += cluster_len; - } - - clusters.push(Cluster { - domain: Domain::from(start..self.domain.end), - stage: self.stage, - }); - - clusters - } - - // This is used in unit tests at present. Ideally it probably shouldn't exist. - #[allow(dead_code)] - pub fn set_stage(&mut self, stage: Stage) -> &mut Self { - self.stage = stage; - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Test for Cluster::subdivide() -} diff --git a/src/mapping/domain.rs b/src/mapping/domain.rs deleted file mode 100644 index ba17a55..0000000 --- a/src/mapping/domain.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::ops::{Deref, DerefMut, Range}; - -use serde::{Deserialize, Serialize}; - -/// Domain, in sectors. -/// Requires sector_size to be provided elsewhere for conversion to bytes. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct Domain(Range) -where - Idx: PartialEq + Eq + PartialOrd + Ord; - -impl Domain -where - Idx: PartialEq + PartialOrd + Ord, -{ - /// Returns the type of overlap between this domain and another. - pub fn overlap(&self, other: Self) -> DomainOverlap { - if self.end <= other.start || other.end <= self.start { - // Cases 7, 8, 12, and 13 of map::tests::test_update - DomainOverlap::None - } else if other.start >= self.start && other.end <= self.end { - // Cases 3, 5, 9, and 11 of map::tests::test_update - DomainOverlap::SelfEngulfsOther - } else if other.start <= self.start && other.end >= self.end { - // Cases 4, 6, and 10 of map::tests::test_update - DomainOverlap::OtherEngulfsSelf - } else if self.start < other.start { - // Case 1 of map::tests::test_update - DomainOverlap::OtherOverlapsEnd - } else { - // Case 2 of map::tests::test_update - DomainOverlap::OtherOverlapsStart - } - } -} - -impl Deref for Domain { - type Target = Range; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Domain { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From> for Domain { - fn from(value: Range) -> Self { - Self(value) - } -} - -pub enum DomainOverlap { - None, - SelfEngulfsOther, - OtherEngulfsSelf, - OtherOverlapsStart, - OtherOverlapsEnd, -} - -#[cfg(test)] -mod tests { - use super::*; -} diff --git a/src/mapping/map.rs b/src/mapping/map.rs deleted file mode 100644 index ff9eceb..0000000 --- a/src/mapping/map.rs +++ /dev/null @@ -1,853 +0,0 @@ -use std::fs::File; -use std::io::Write; - -use crate::mapping::cluster; - -use super::{Cluster, Domain, DomainOverlap, Stage}; - -use anyhow; -use ron::de::from_reader; -use ron::error::SpannedError; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct MapFile { - pub sector_size: usize, - pub domain: Domain, - pub map: Vec, -} - -impl TryFrom for MapFile { - type Error = SpannedError; - - fn try_from(file: File) -> Result { - from_reader(file) - } -} - -impl Default for MapFile { - fn default() -> Self { - MapFile { - sector_size: crate::FB_SECTOR_SIZE, - domain: Domain::from(0..1), - map: vec![Cluster { - domain: Domain::from(0..1), - stage: Stage::Patchwork { depth: 0 }, - }], - } - } -} - -impl MapFile { - pub fn new(sector_size: usize) -> Self { - MapFile::default().set_sector_size(sector_size).to_owned() - } - - pub fn set_sector_size(&mut self, sector_size: usize) -> &mut Self { - self.sector_size = sector_size; - self - } - - /// Recalculate cluster mappings. - pub fn update(&mut self, new: Cluster) -> &mut Self { - let mut map: Vec = vec![Cluster::from(new.clone())]; - - for old in self.map.iter() { - let mut old = *old; - - match new.domain.overlap(old.domain) { - DomainOverlap::None => map.push(old), - DomainOverlap::SelfEngulfsOther => (), - DomainOverlap::OtherEngulfsSelf => { - other_engulfs_self_update(new, &mut old, &mut map) - } - DomainOverlap::OtherOverlapsEnd => { - // Case 1 - old.domain.start = new.domain.end; - map.push(old); - } - DomainOverlap::OtherOverlapsStart => { - // Case 2 - old.domain.end = new.domain.start; - map.push(old); - } - }; - } - - self.map = map; - self - } - - /// Get current recovery stage. - pub fn get_stage(&self) -> Stage { - let mut recover_stage = Stage::Damaged; - - for cluster in self.map.iter() { - if cluster.stage < recover_stage { - recover_stage = cluster.stage; - } - } - - recover_stage - } - - /// Get clusters of common stage. - pub fn get_clusters(&self, stage: Stage) -> Vec { - self.map - .iter() - .filter_map(|mc| { - if mc.stage == stage { - Some(mc.to_owned()) - } else { - None - } - }) - .collect() - } - - /// Defragments cluster groups. - /// I.E. check forwards every cluster from current until stage changes, - /// then group at once. - pub fn defrag(&mut self) { - self.map.sort_by_key(|c| c.domain.start); - - let mut new_map: Vec = vec![]; - let mut idx = 0; - let mut master; - while idx < self.map.len() - 1 { - master = self.map[idx]; - - for c in self.map[idx + 1..self.map.len()].into_iter() { - if c.stage != master.stage { - break; - } - - idx += 1; - } - - master.domain.end = self.map[idx].domain.end; - new_map.push(master); - idx += 1; - } - - self.map = new_map; - } - - /// Extend the domain of the MapFile. - /// Returns None if the domain cannot be changed or is unchanged. - /// Returns the delta of the previous domain end and the new end. - pub fn extend(&mut self..usize) -> Option { - if end <= self.domain.end { - return None; - } - - let old_end = self.domain.end; - let delta = end - old_end; - self.domain.end = end; - - // Add new data as untested. - self.update(Cluster { - domain: Domain::from(old_end..self.domain.end), - ..Default::default() - }); - Some(delta) - } - - /// Writes the map to the provided item implementing `Write` trait. - /// Usually a file. - pub fn write_to(&mut self, file: &mut W) -> anyhow::Result { - self.defrag(); - - let written_bytes = file.write( - ron::ser::to_string_pretty( - self, - ron::ser::PrettyConfig::new() - .new_line("\n".to_string()) - .struct_names(true), - )? - .as_bytes(), - )?; - - Ok(written_bytes) - } -} - -// This is split out for a shred of readability. -fn other_engulfs_self_update(new: Cluster, old: &mut Cluster, map: &mut Vec) { - if new.domain.start == old.domain.start { - // Case 6 of map::tests::test_update - old.domain.start = new.domain.end; - } else { - // Case 4 and part of 10 - - let old_end = old.domain.end; - old.domain.end = new.domain.start; - - if new.domain.end != old_end { - // Case 10 of map::tests::test_update - map.push(Cluster { - domain: Domain::from(new.domain.end..old_end), - stage: old.stage, - }) - } - } - - map.push(old.to_owned()) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Test for MapFile::update() - #[test] - fn update_1_new_overlaps_start() { - // Case 1: - // |----new----| - // |----old----| - // - // | --> |-old-| - // Solution: old.start = new.end - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(1..3), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(0..2), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![ - Cluster { - domain: Domain::from(0..2), - ..Default::default() - }, - Cluster { - domain: Domain::from(2..3), - ..Default::default() - } - ] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_2_new_overlaps_end() { - // Case 2: - // |----new----| - // |----old----| - // - // |-old-| <-- | - // Solution: old.end = new.start - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(0..2), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(1..3), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![ - Cluster { - domain: Domain::from(0..1), - ..Default::default() - }, - Cluster { - domain: Domain::from(1..3), - ..Default::default() - } - ] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_3_new_engulfs_common_end() { - // Case 3: - // |----new----| - // |--old--| - // - // Solution: Remove old. - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(1..3), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(0..3), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![Cluster { - domain: Domain::from(0..3), - ..Default::default() - }] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_4_old_engulfs_common_end() { - // Case 4: - // |--new--| - // |-----old-----| - // - // |-old-| <---- | - // Solution: old.end = new.start - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(0..3), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(1..3), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![ - Cluster { - domain: Domain::from(0..1), - ..Default::default() - }, - Cluster { - domain: Domain::from(1..3), - ..Default::default() - } - ] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_5_new_engulfs_common_start() { - // Case 5: - // |-----new----| - // |--old--| - // - // Solution: Remove old. - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(0..2), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(0..3), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![Cluster { - domain: Domain::from(0..3), - ..Default::default() - }] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_6_old_engulfs_common_start() { - // Case 6: - // |--new--| - // |-----old-----| - // - // | ----> |-old-| - // Solution: old.start = new.end - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(0..3), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(0..2), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![ - Cluster { - domain: Domain::from(0..2), - ..Default::default() - }, - Cluster { - domain: Domain::from(2..3), - ..Default::default() - } - ] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_7_new_precedes() { - // Case 7: - // |--new--| - // |--old--| - // - // Solution: Leave unchanged. - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(2..3), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(0..2), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![ - Cluster { - domain: Domain::from(0..2), - ..Default::default() - }, - Cluster { - domain: Domain::from(2..3), - ..Default::default() - } - ] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_8_new_trails() { - // Case 8: - // |--new--| - // |--old--| - // Solution: Leave unchanged. - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(0..2), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(2..3), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![ - Cluster { - domain: Domain::from(0..2), - ..Default::default() - }, - Cluster { - domain: Domain::from(2..3), - ..Default::default() - } - ] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_9_new_engulfs() { - // Case 9: - // |-----new-----| - // |--old--| - // - // Solution: Remove old. - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(1..2), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(0..3), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![Cluster { - domain: Domain::from(0..3 ), - ..Default::default() - }] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_10_old_engulfs() { - // Case 10: - // |--new--| - // |--------------old--------------| - // - // |----old----| <---- | - // + |--fracture-| - // Solution: old.end = new.start - // && fracture: - // with fracture.start = new.end - // && fracture.end = old.original_end - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(0..3), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(1..2 ), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![ - Cluster { - domain: Domain::from(0..1 ), - ..Default::default() - }, - Cluster { - domain: Domain::from(1..2 ), - ..Default::default() - }, - Cluster { - domain: Domain::from(2..3 ), - ..Default::default() - } - ] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_11_common_start_and_end() { - // Case 11: - // |--new--| - // |--old--| - // - // Solution: Remove old. - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(0..3 ), - stage: Stage::Patchwork { depth: 0 }, - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(0..3 ), - stage: Stage::Intact, - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![Cluster { - domain: Domain::from(0..3 ), - stage: Stage::Intact - }] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_12_new_out_of_range_preceding() { - // Case 12: - // |--new--| - // |--old--| - // - // Solution: Leave Unchanged. - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(2..3 ), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(0..1 ), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![ - Cluster { - domain: Domain::from(0..1 ), - ..Default::default() - }, - Cluster { - domain: Domain::from(2..3 ), - ..Default::default() - } - ] - ); - } - - /// Test for MapFile::update() - #[test] - fn update_13_new_out_of_range_trailing() { - // Case 13: - // |--new--| - // |--old--| - // - // Solution: Leave Unchanged. - - let mut map = MapFile { - map: vec![Cluster { - domain: Domain::from(0..1 ), - ..Default::default() - }], - ..Default::default() - }; - - map.update(Cluster { - domain: Domain::from(2..3 ), - ..Default::default() - }); - map.map.sort(); - - assert_eq!( - map.map, - vec![ - Cluster { - domain: Domain::from(0..1 ), - ..Default::default() - }, - Cluster { - domain: Domain::from(2..3 ), - ..Default::default() - } - ] - ); - } - - /// Test for MapFile::get_stage() - #[test] - fn get_stage() { - let mut mf = MapFile::default(); - let mut mf_stage = mf.get_stage(); - - // If this fails here, there's something SERIOUSLY wrong. - assert!( - mf_stage == Stage::Patchwork { depth: 0 }, - "Determined stage to be {:?}, when {:?} was expected.", - mf_stage, - Stage::Patchwork { depth: 0 } - ); - - let stages = vec![ - Stage::Damaged, - Stage::Patchwork { depth: 1 }, - Stage::Patchwork { depth: 0 }, - ]; - - mf.map = vec![]; - - for stage in stages { - mf.map.push(*Cluster::default().set_stage(stage)); - - mf_stage = mf.get_stage(); - - assert!( - stage == mf_stage, - "Expected stage to be {:?}, determined {:?} instead.", - stage, - mf_stage - ) - } - } - - /// Test for MapFile::get_clusters() - #[test] - fn get_clusters() { - let mut mf = MapFile::default(); - - mf.map = vec![ - *Cluster::default().set_stage(Stage::Damaged), - *Cluster::default().set_stage(Stage::Patchwork { depth: 1 }), - Cluster::default(), - Cluster::default(), - *Cluster::default().set_stage(Stage::Patchwork { depth: 1 }), - *Cluster::default().set_stage(Stage::Damaged), - ]; - - let stages = vec![ - Stage::Damaged, - Stage::Patchwork { depth: 1 }, - Stage::Patchwork { depth: 0 }, - ]; - - for stage in stages { - let expected = vec![ - *Cluster::default().set_stage(stage), - *Cluster::default().set_stage(stage), - ]; - let received = mf.get_clusters(stage); - - assert!( - expected == received, - "Expected clusters {:?}, got {:?}.", - expected, - received - ) - } - } - - /// Test for MapFile::defrag() - #[test] - fn defrag() { - let mut mf = MapFile { - sector_size: 1, - domain: Domain::from(0..8 ), - map: vec![ - Cluster { - domain: Domain::from(0..1 ), - stage: Stage::Patchwork { depth: 0 }, - }, - Cluster { - domain: Domain::from(1..1 ), - stage: Stage::Patchwork { depth: 0 }, - }, - Cluster { - domain: Domain::from(2..3 ), - stage: Stage::Patchwork { depth: 0 }, - }, - Cluster { - domain: Domain::from(3..4 ), - stage: Stage::Isolate, - }, - Cluster { - domain: Domain::from(4..5 ), - stage: Stage::Isolate, - }, - Cluster { - domain: Domain::from(5..6 ), - stage: Stage::Patchwork { depth: 1 }, - }, - Cluster { - domain: Domain::from(6..7 ), - stage: Stage::Patchwork { depth: 0 }, - }, - Cluster { - domain: Domain::from(7..8 ), - stage: Stage::Damaged, - }, - Cluster { - domain: Domain::from(8..10 ), - stage: Stage::Intact, - }, - Cluster { - domain: Domain::from(10..10 ), - stage: Stage::BruteForceAndDesperation, - }, - Cluster { - domain: Domain::from(11..12 ), - stage: Stage::BruteForceAndDesperation, - }, - ], - }; - - let expected = vec![ - Cluster { - domain: Domain::from(0..3 ), - stage: Stage::Patchwork { depth: 0 }, - }, - Cluster { - domain: Domain::from(3..5 ), - stage: Stage::Isolate, - }, - Cluster { - domain: Domain::from(5..6 ), - stage: Stage::Patchwork { depth: 1 }, - }, - Cluster { - domain: Domain::from(6..7 ), - stage: Stage::Patchwork { depth: 0 }, - }, - Cluster { - domain: Domain::from(7..8 ), - stage: Stage::Damaged, - }, - Cluster { - domain: Domain::from(8..10 ), - stage: Stage::Intact, - }, - Cluster { - domain: Domain::from(10..12 ), - stage: Stage::BruteForceAndDesperation, - }, - ]; - - mf.defrag(); - mf.map.sort_by_key(|c| c.domain.start); - - let received = mf.map; - - assert!( - expected == received, - "Expected {:?} after defragging, got {:?}.", - expected, - received - ) - } -} diff --git a/src/mapping/mod.rs b/src/mapping/mod.rs deleted file mode 100644 index df836b0..0000000 --- a/src/mapping/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![allow(unused_imports)] - -pub mod cluster; -pub mod domain; -pub mod map; -pub mod prelude; -pub mod stage; - -pub use cluster::Cluster; -pub use domain::{Domain, DomainOverlap}; -pub use map::MapFile; -pub use stage::Stage; diff --git a/src/mapping/prelude.rs b/src/mapping/prelude.rs deleted file mode 100644 index 9d4b452..0000000 --- a/src/mapping/prelude.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![allow(unused_imports)] - -pub use super::cluster::Cluster; -pub use super::domain::{Domain, DomainOverlap}; -pub use super::map::MapFile; -pub use super::stage::Stage; diff --git a/src/mapping/stage.rs b/src/mapping/stage.rs deleted file mode 100644 index 7e40f64..0000000 --- a/src/mapping/stage.rs +++ /dev/null @@ -1,22 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub enum Stage { - // Don't mess with the order. - Patchwork { depth: usize }, - Isolate, - BruteForceAndDesperation, - Damaged, - Intact, -} - -impl Default for Stage { - fn default() -> Self { - Stage::Patchwork { depth: 0 } - } -} - -#[cfg(test)] -mod tests { - use super::*; -} diff --git a/src/path.rs b/src/path.rs deleted file mode 100644 index b130d75..0000000 --- a/src/path.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::LazyLock; - -use crate::cli::CONFIG; - -use anyhow::{self, Context}; - -/// Generates a file path if one not provided. -/// root_path for fallback name. -pub fn get_path

(path: &Option

, root_path: &P, extension: &str) -> anyhow::Result -where - P: AsRef, -{ - if let Some(f) = path { - return Ok(f.as_ref().to_path_buf()); - } - - Ok(PathBuf::from(format!( - "{}.{}", - root_path - .as_ref() - .to_str() - .context("source_name path was not UTF-8 valid.")?, - extension - )) - .as_path() - .to_owned()) -} - -pub static MAP_PATH: LazyLock = LazyLock::new(|| { - get_path(&CONFIG.map, &CONFIG.input, "map") - .context("Failed to generate map path.") - .unwrap() -}); - -pub static OUTPUT_PATH: LazyLock = LazyLock::new(|| { - get_path(&CONFIG.output, &CONFIG.input, "iso") - .context("Failed to generate output path.") - .unwrap() -}); diff --git a/src/recovery.rs b/src/recovery.rs deleted file mode 100644 index 4447c73..0000000 --- a/src/recovery.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::fs::File; -use std::io::{BufWriter, Read, Seek, SeekFrom, Write}; -use std::usize; - -use anyhow::Context; - -use crate::cli::CONFIG; -use crate::io::DirectIOBuffer; -use crate::mapping::prelude::*; - -#[derive(Debug)] -pub struct Recover { - input: File, - output: BufWriter, - map: MapFile, -} - -impl Recover { - pub fn new() -> anyhow::Result { - let input: File = crate::io::load_input()?; - let output: File = crate::io::load_output()?; - - let map: MapFile = { - if let Ok(f) = crate::io::load_map_read() - && let Ok(map_file) = MapFile::try_from(f) - { - map_file - } else { - MapFile::new(CONFIG.sector_size) - } - }; - - let mut r = Recover { - input, - output: BufWriter::with_capacity(map.domain.end as usize, output), - map, - }; - - r.restore()?; - - Ok(r) - } - - /// Recover media. - pub fn run(&mut self) -> anyhow::Result { - let mut is_finished = false; - - while !is_finished { - self.map.defrag(); - - match self.map.get_stage() { - Stage::Patchwork { depth } => self.copy_patchwork(depth)?, - Stage::Isolate => todo!(), - Stage::BruteForceAndDesperation => todo!(), - Stage::Damaged | Stage::Intact => { - println!("Cannot recover further."); - - is_finished = true - } - }; - - // Need to reset seek position between algorithms. - self.input - .rewind() - .context("Failed to reset input seek position.")?; - self.output - .rewind() - .context("Failed to reset output seek position")?; - } - - // Temporary. - let recovered_bytes = usize::MIN; - Ok(recovered_bytes) - } - - /// Restore current progress based on MapFile. - /// Also updates MapFile if needed, such as to extend the MapFile domain. - pub fn restore(&mut self) -> anyhow::Result<()> { - self.map.extend( - crate::io::get_stream_length(&mut self.input) - .context("Failed to get input stream length.")? as usize, - ); - - Ok(()) - } - - /// Attempt to copy all untested blocks. - fn copy_patchwork(&mut self, mut depth: usize) -> anyhow::Result<()> { - let mut buf = DirectIOBuffer::new(); - let mut buf_capacity = self.get_buf_capacity() as usize; - - while self.map.get_stage() == (Stage::Patchwork { depth }) { - // Order of these two expressions matters, stupid. - buf_capacity /= depth + 1; - - for cluster in self.map.get_clusters(Stage::Patchwork { depth }) { - self.read_domain(buf.as_mut(), cluster.domain, buf_capacity, Stage::Isolate)?; - } - - depth += 1; - } - - Ok(()) - } - - fn read_domain( - &mut self, - buf: &mut [u8], - domain: Domain, - mut buf_capacity: usize, - next_stage: Stage, - ) -> anyhow::Result<()> { - let mut cluster; - let mut read_position = domain.start; - - while read_position < domain.end { - buf_capacity = buf_capacity.min(domain.end - read_position); - - cluster = Cluster { - domain: Domain { - start: read_position, - end: read_position + buf_capacity, - }, - stage: Stage::Intact, - }; - - dbg!(cluster.domain); - - match self.read_sectors(buf.as_mut()) { - Ok(bytes) => { - self.output - .write_all(&buf[0..bytes]) - .context("Failed to write data to output file")?; - read_position += bytes; - } - Err(err) => { - println!("Hit error: {:?}", err); - if CONFIG.reopen_on_error { - self.reload_input() - .context("Failed to reload input file after previous error")?; - } - - self.input - .seek_relative(buf_capacity as i64) - .context("Failed to seek input by buf_capacity to skip previous error")?; - self.output - .seek_relative(buf_capacity as i64) - .context("Failed to seek output by buf_capacity to skip previous error")?; - - cluster.stage = next_stage.clone(); - } - } - - self.map.update(cluster); - self.map.write_to(&mut crate::io::load_map_write()?)?; - } - - Ok(()) - } - - /// Set buffer capacity as cluster length in bytes. - /// Varies depending on the recovery stage. - fn get_buf_capacity(&mut self) -> u64 { - crate::MAX_BUFFER_SIZE.min(CONFIG.sector_size * CONFIG.cluster_length) as u64 - } - - /// Reloads the input and restores the seek position. - fn reload_input(&mut self) -> anyhow::Result<()> { - let seek_pos = self.input.stream_position()?; - - self.input = crate::io::load_input()?; - self.input.seek(SeekFrom::Start(seek_pos))?; - Ok(()) - } - - fn read_sectors(&mut self, mut buf: &mut [u8]) -> std::io::Result { - let mut raw_buf = vec![crate::FB_NULL_VALUE; buf.len()]; - let result = self.input.read(&mut raw_buf); - - if result.is_err() { - return result; - } else if let Ok(mut bytes) = result - && bytes >= CONFIG.sector_size - { - // Remember that this is integer division (floor division) - bytes = (bytes / CONFIG.sector_size) * CONFIG.sector_size; - buf.write_all(&raw_buf[..bytes]).unwrap(); - - return Ok(bytes); - } else { - return Ok(0); - } - } -} - -#[cfg(test)] -#[allow(unused)] -mod tests { - use super::*; - - // Test for Recover::set_buf_capacity -} diff --git a/src/tui.rs b/src/tui.rs deleted file mode 100644 index d9c1809..0000000 --- a/src/tui.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::io; -use std::sync::mpsc; - -use ratatui::crossterm; -use ratatui::layout::{Constraint, Layout}; -use ratatui::symbols::border; -use ratatui::widgets::{Block, Widget}; -use ratatui::{DefaultTerminal, Frame}; - -#[derive(Debug, Default)] -pub struct Tui { - // bool::default() -> false - exit: bool, -} - -impl Tui { - pub fn new() -> Self { - Self::default() - } - - pub fn run( - &mut self, - terminal: &mut DefaultTerminal, - rx: mpsc::Receiver, - ) -> io::Result<()> { - while !self.exit { - // Render frame. - terminal.draw(|frame| self.draw(frame))?; - - // Event handler - // unwraps, bc what could go wrong? - match rx.recv().unwrap() { - Event::Input(key_event) => self.handle_key_event(key_event)?, - } - } - - Ok(()) - } - - pub fn draw(&self, frame: &mut Frame) { - frame.render_widget(self, frame.area()); - } - - pub fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) -> io::Result<()> { - if key_event.kind == crossterm::event::KeyEventKind::Press - && key_event.code == crossterm::event::KeyCode::Char('q') - { - self.exit = true; - } - - Ok(()) - } -} - -// impl on reference to avoid accidentally mutating. -impl Widget for &Tui { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) - where - Self: Sized, - { - let [main_area, _info_area] = - Layout::vertical([Constraint::Percentage(75), Constraint::Fill(1)]).areas(area); - - let main_block = Block::bordered() - .title(" Viewer ") - .border_set(border::THICK); - main_block.render(main_area, buf); - } -} - -pub enum Event { - Input(crossterm::event::KeyEvent), -} - -pub fn input_fetcher(tx: mpsc::Sender) { - loop { - // unwraps, bc what could go wrong? - match crossterm::event::read().unwrap() { - crossterm::event::Event::Key(key_event) => tx.send(Event::Input(key_event)).unwrap(), - _ => (), - } - } -} diff --git a/templates/imports.rs b/templates/imports.rs deleted file mode 100644 index 4e9db5f..0000000 --- a/templates/imports.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Acknowledge sister/child -mod module; - -// std -use std::*; - -// sister/child -use module1::*; - -// parent -use super::*; - -// ancestor of parent -use crate::*; - -// external -use external::*;