Initial commit.

This commit is contained in:
2026-04-20 22:57:43 -04:00
parent 4cd1f90d70
commit 216fb4068f
18 changed files with 87 additions and 1702 deletions
+1 -1
View File
@@ -1 +1 @@
/target
**/target
+24 -21
View File
@@ -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
+49 -48
View File
@@ -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
+10
View File
@@ -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]
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}
-58
View File
@@ -1,58 +0,0 @@
use std::path::PathBuf;
use std::sync::LazyLock;
use clap::{ArgAction, Parser};
pub static CONFIG: LazyLock<Args> = 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<PathBuf>,
/// Path to rescue map. Defaults to {input}.map
#[arg(short, long, value_hint = clap::ValueHint::DirPath)]
pub map: Option<PathBuf>,
/// 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,
}
-154
View File
@@ -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<S: Seek>(stream: &mut S) -> io::Result<u64> {
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<File> {
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<File> {
OpenOptions::new()
.read(true)
.open(&CONFIG.input)
.with_context(|| format!("Failed to open input file: {}", &CONFIG.input.display()))
}
pub fn load_output() -> anyhow::Result<File> {
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<File> {
OpenOptions::new()
.read(true)
.open(crate::path::MAP_PATH.clone())
}
pub fn load_map_write() -> anyhow::Result<File> {
// 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<P: AsRef<Path>>(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<Self, Self::Error> {
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<Idx> Index<Idx> 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]
}
}
-55
View File
@@ -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::<crate::tui::Event>();
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
}
-62
View File
@@ -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<usize>,
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<Cluster> {
let domain_len = self.domain.len();
let mut start = self.domain.start;
let mut clusters: Vec<Cluster> = 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()
}
-68
View File
@@ -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<Idx>(Range<Idx>)
where
Idx: PartialEq + Eq + PartialOrd + Ord;
impl<Idx> Domain<Idx>
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<Idx> Deref for Domain<Idx> {
type Target = Range<Idx>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<Idx> DerefMut for Domain<Idx> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<Idx> From<Range<Idx>> for Domain<Idx> {
fn from(value: Range<Idx>) -> Self {
Self(value)
}
}
pub enum DomainOverlap {
None,
SelfEngulfsOther,
OtherEngulfsSelf,
OtherOverlapsStart,
OtherOverlapsEnd,
}
#[cfg(test)]
mod tests {
use super::*;
}
-853
View File
@@ -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<usize>,
pub map: Vec<Cluster>,
}
impl TryFrom<File> for MapFile {
type Error = SpannedError;
fn try_from(file: File) -> Result<Self, Self::Error> {
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<Cluster> = 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<Cluster> {
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<Cluster> = 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<usize> {
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<W: Write>(&mut self, file: &mut W) -> anyhow::Result<usize> {
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<Cluster>) {
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
)
}
}
-12
View File
@@ -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;
-6
View File
@@ -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;
-22
View File
@@ -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::*;
}
-40
View File
@@ -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<P>(path: &Option<P>, root_path: &P, extension: &str) -> anyhow::Result<PathBuf>
where
P: AsRef<Path>,
{
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<PathBuf> = LazyLock::new(|| {
get_path(&CONFIG.map, &CONFIG.input, "map")
.context("Failed to generate map path.")
.unwrap()
});
pub static OUTPUT_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
get_path(&CONFIG.output, &CONFIG.input, "iso")
.context("Failed to generate output path.")
.unwrap()
});
-202
View File
@@ -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<File>,
map: MapFile,
}
impl Recover {
pub fn new() -> anyhow::Result<Self> {
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<usize> {
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<usize> {
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
}
-83
View File
@@ -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<Event>,
) -> 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<Event>) {
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(),
_ => (),
}
}
}
-17
View File
@@ -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::*;