Compare commits
1 Commits
main
...
v0.3_refactor
| Author | SHA1 | Date | |
|---|---|---|---|
| 216fb4068f |
+1
-1
@@ -1 +1 @@
|
||||
/target
|
||||
**/target
|
||||
|
||||
+24
-21
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
-58
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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::*;
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
Reference in New Issue
Block a user