Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6190e4d968
|
+1
-2
@@ -1,2 +1 @@
|
||||
**/target
|
||||
**/*.lock
|
||||
/target
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "external/libdvdcss"]
|
||||
path = external/libdvdcss
|
||||
url = https://code.videolan.org/videolan/libdvdcss.git
|
||||
Generated
+1783
File diff suppressed because it is too large
Load Diff
+21
-31
@@ -1,36 +1,29 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/*", "plugins/scraper"]
|
||||
default-members = ["crates/kramer"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.0"
|
||||
[package]
|
||||
name = "kramer"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["Olivia Brooks"]
|
||||
repository = "https://gitea.cutieguwu.ca/cutieguwu/kramer"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
#
|
||||
# Workspace member crates
|
||||
#
|
||||
dvd = { path = "crates/dvd" }
|
||||
dvdcss-sys = { path = "crates/dvdcss-sys" }
|
||||
dvdcss = { path = "crates/dvdcss" }
|
||||
mapping = { path = "crates/mapping" }
|
||||
media = { path = "crates/media" }
|
||||
plugins = { path = "crates/plugins" }
|
||||
scsi = { path = "crates/scsi" }
|
||||
[dependencies]
|
||||
num-traits = "0.2.19"
|
||||
ratatui = "0.30"
|
||||
ron = ">=0.8, <0.13"
|
||||
#rust-i18n = "3.1.3"
|
||||
|
||||
#
|
||||
# External crates
|
||||
#
|
||||
#anyhow = { version = "1.0", features = ["backtrace"] }
|
||||
#clap = { version = "4.5", features = ["derive"] }
|
||||
derive_more = { version = "2.1", features = ["display", "from"] }
|
||||
semver = { version = "1.0" }
|
||||
#serde = { version = "1.0", features = ["derive"] }
|
||||
[dependencies.anyhow]
|
||||
version = "1.0"
|
||||
features = ["backtrace"]
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.5"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.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
|
||||
@@ -38,8 +31,5 @@ semver = { version = "1.0" }
|
||||
#
|
||||
# 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"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||
libc = "~0.2.25"
|
||||
|
||||
@@ -7,65 +7,64 @@ will continue to be referred to as kramer.
|
||||
|
||||
This is still in very early development, so expect old maps to no longer work.
|
||||
|
||||
## Where is it at?
|
||||
## Plans
|
||||
|
||||
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.
|
||||
### Core
|
||||
|
||||
As such, you have been warned.
|
||||
- [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.
|
||||
|
||||
## What have I managed to achieve so far?
|
||||
### Extra
|
||||
|
||||
So far, by blindly bumbling my ??? through the Sharran darkness, I have managed:
|
||||
- [ ] i18n
|
||||
- [ ] English
|
||||
- [ ] French
|
||||
- [ ] TUI (akin to `ddrescueview`)
|
||||
- [ ] Visual status map
|
||||
- [ ] Recovery properties
|
||||
- [ ] Recovery progress
|
||||
- [ ] Recovery stats
|
||||
|
||||
### `DIRECT_IO` reading in Rust on platforms which support it
|
||||
## Recovery Strategy
|
||||
|
||||
Effectively bypassing the kernel buffer and other safeties offered by the
|
||||
kernel which inhibit reading past IO failure.
|
||||
### Initial Pass / Patchworking
|
||||
|
||||
### Tracking of recovery progress by mapping disc regions
|
||||
Tries to read clusters of `max_buffer_size`, marking clusters with errors with
|
||||
an increasing `level`.
|
||||
|
||||
This includes working map defragmentation. I did *not* enjoy figuring out all
|
||||
13 cases for that.
|
||||
This works by halving the length of the read buffer until one of two
|
||||
conditions is met:
|
||||
|
||||
This code and its associated methods need to be reviewed. Current iteration is
|
||||
a *really* hack and unreadable solution.
|
||||
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`
|
||||
|
||||
### Possibly wrapping `libdvdcss` for SCSI bus encryption
|
||||
### Isolate
|
||||
|
||||
This is yet untested, but I have written a wrapper with the help of `bindgen`.
|
||||
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.
|
||||
|
||||
I really have two options here:
|
||||
### Scraping (Stage::BruteForceAndDesperation)
|
||||
|
||||
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
|
||||
This is the pure brute force, sector-at-a-time read. This has identical
|
||||
behaviour to `ddrescue`'s scraping phase.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "dvdcss-sys"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.72.1"
|
||||
meson = "1.0"
|
||||
@@ -1,3 +0,0 @@
|
||||
# dvdcss-sys
|
||||
|
||||
A crate for pure bindings against the system libdvdcss.
|
||||
@@ -1,33 +0,0 @@
|
||||
use std::env::{self, current_dir};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
// Locate and link the shared library.
|
||||
println!("cargo:rustc-link-lib=dvdcss");
|
||||
|
||||
// Because clang wants an absolute path and using canonicalize() fails.
|
||||
let libdir_path = current_dir()
|
||||
.expect("Cannot get CWD.")
|
||||
.join("../../external/libdvdcss");
|
||||
|
||||
// Run the meson build to produce build/config.h
|
||||
meson::build(
|
||||
libdir_path.to_str().unwrap(),
|
||||
libdir_path.join("build").to_str().unwrap(),
|
||||
);
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/wrapper.h")
|
||||
.clang_arg(format!("-I{}/src", libdir_path.to_str().unwrap()))
|
||||
.clang_arg(format!("-I{}/build", libdir_path.to_str().unwrap()))
|
||||
// Invalidate the build if a header has changed.
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Failed to generate bindings.");
|
||||
|
||||
// Write the bindings to the $OUT_DIR/bindings.rs file.
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("bindings.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(unused)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
@@ -1,2 +0,0 @@
|
||||
#include "dvdcss/dvdcss.h"
|
||||
#include "libdvdcss.h"
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "dvdcss"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
derive_more.workspace = true
|
||||
semver.workspace = true
|
||||
dvdcss-sys.workspace = true
|
||||
|
||||
[features]
|
||||
semver = []
|
||||
encryption-type = []
|
||||
@@ -1,11 +0,0 @@
|
||||
# libdvdcss Rust Wrapper
|
||||
|
||||
This crate assumes a baseline version of `libdvdcss` 1.5.0.
|
||||
|
||||
If you wish to use the `dvdcss_get_encryption_type()` function introduced in
|
||||
`libdvdcss` version 1.6.0, add the `encryption-type` feature.
|
||||
|
||||
For convenience, the `semver` feature can be added for a `semver::Version`
|
||||
representation of `DVDCSS_VERSION` instead of a tuple.
|
||||
|
||||
`dvdcss_open_stream()` is not implemented. At least not yet.
|
||||
@@ -1,180 +0,0 @@
|
||||
use std::ffi::{CStr, c_int, c_void};
|
||||
use std::io::{self, Error, Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
||||
use derive_more::From;
|
||||
use dvdcss_sys::*;
|
||||
|
||||
pub const BLOCK_SIZE: u32 = DVDCSS_BLOCK_SIZE;
|
||||
|
||||
#[cfg(feature = "semver")]
|
||||
pub const DVDCSS_VERSION: semver::Version = semver::Version::new(
|
||||
DVDCSS_VERSION_MAJOR as u64,
|
||||
DVDCSS_VERSION_MINOR as u64,
|
||||
DVDCSS_VERSION_MICRO as u64,
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "semver"))]
|
||||
pub const DVDCSS_VERSION: (u32, u32, u32) = (
|
||||
DVDCSS_VERSION_MAJOR,
|
||||
DVDCSS_VERSION_MINOR,
|
||||
DVDCSS_VERSION_MICRO,
|
||||
);
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Default)]
|
||||
pub enum SeekFlag {
|
||||
#[default]
|
||||
None = DVDCSS_NOFLAGS,
|
||||
Key = DVDCSS_SEEK_KEY,
|
||||
Mpeg = DVDCSS_SEEK_MPEG,
|
||||
}
|
||||
|
||||
// I dislike this as technically, 2 should be Unknown/Reserved and 3 as CPRM.
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Default, From)]
|
||||
pub enum EncryptionScheme {
|
||||
#[default]
|
||||
None = 0,
|
||||
CssCppm = 1,
|
||||
//Reserved,
|
||||
Cprm = 2,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub struct Dvd {
|
||||
ptr: dvdcss_t,
|
||||
decrypt: bool,
|
||||
}
|
||||
|
||||
impl Dvd {
|
||||
/// Open a DVD.
|
||||
/// If libdvdcss returns a null pointer, this returns None.
|
||||
/// No more about the error can be discerned than this.
|
||||
pub fn open<P: AsRef<Path>>(src: P) -> Option<Self> {
|
||||
// Cast the path to a CStr (*const char),
|
||||
// returning early if there's any issue.
|
||||
let c_path = CStr::from_bytes_with_nul(src.as_ref().as_os_str().as_encoded_bytes())
|
||||
.ok()?
|
||||
.as_ptr();
|
||||
|
||||
let ptr = unsafe { dvdcss_open(c_path) };
|
||||
|
||||
if ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(Self {
|
||||
ptr,
|
||||
decrypt: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `Dvd` from a raw pointer to `dvdcss_t`.
|
||||
pub unsafe fn from_raw_pointer(ptr: dvdcss_t) -> Self {
|
||||
Self {
|
||||
ptr,
|
||||
decrypt: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt(&mut self, decrypt: bool) {
|
||||
self.decrypt = decrypt;
|
||||
}
|
||||
|
||||
/// Return the error message string from dvdcss_error
|
||||
pub fn error(&self) -> &CStr {
|
||||
unsafe { CStr::from_ptr(dvdcss_error(self.ptr)) }
|
||||
}
|
||||
|
||||
/// Return the error message string from dvdcss_error with lossy conversion.
|
||||
pub fn error_lossy(&self) -> String {
|
||||
self.error().to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Detect whether or not the content is scrambled.
|
||||
pub fn is_scrambled(&self) -> bool {
|
||||
match unsafe { dvdcss_is_scrambled(self.ptr) } {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The length in bytes of the Dvd.
|
||||
pub fn len(&self) {
|
||||
self.ptr.i_fd
|
||||
}
|
||||
|
||||
/// Return the encryption scheme in use.
|
||||
#[cfg(feature = "encryption-type")]
|
||||
pub fn encryption_type(&self) -> EncryptionScheme {
|
||||
unsafe { dvdcss_get_encryption_type(self.ptr) }
|
||||
.try_into()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Dvd {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let i_flags = if self.decrypt {
|
||||
DVDCSS_READ_DECRYPT
|
||||
} else {
|
||||
DVDCSS_NOFLAGS
|
||||
} as i32;
|
||||
|
||||
let retval = unsafe {
|
||||
dvdcss_read(
|
||||
self.ptr,
|
||||
buf.as_mut_ptr() as *mut c_void,
|
||||
buf.len() as i32 / BLOCK_SIZE as c_int,
|
||||
i_flags,
|
||||
)
|
||||
};
|
||||
|
||||
if retval < 0 {
|
||||
Err(Error::last_os_error())
|
||||
} else {
|
||||
Ok(retval.unsigned_abs() as usize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Dvd {
|
||||
/// Seek to some posititon in the disc.
|
||||
///
|
||||
/// Contrary to Rust's standard for Seek implementations,
|
||||
/// this is limited to seeking in multiples of BLOCK_SIZE (2048),
|
||||
/// as defined by `dvdcss_seek()`.
|
||||
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
|
||||
// dvdcss_seek reports in blocks, rather than bytes as expected by Rust.
|
||||
//
|
||||
// As for what purpose the flags serve... I haven't a clue.
|
||||
|
||||
let bytes = match pos {
|
||||
SeekFrom::Start(p) => p as i64,
|
||||
SeekFrom::Current(p) => self.stream_position()? as i64 + p,
|
||||
SeekFrom::End(p) => self.stream_len()? as i64 - p,
|
||||
} * BLOCK_SIZE as i64;
|
||||
|
||||
// Although this call generally returns a negative value on fail, as of writing
|
||||
// it can only return -1, so there's no point in expanding this fail result into
|
||||
// a proper enum.
|
||||
let retval = unsafe { dvdcss_seek(self.ptr, bytes as c_int, SeekFlag::None as c_int) };
|
||||
|
||||
if retval < 0 {
|
||||
Err(Error::last_os_error())
|
||||
} else {
|
||||
Ok((retval.unsigned_abs() * BLOCK_SIZE) as u64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Dvd {
|
||||
fn drop(&mut self) {
|
||||
// Right... so drop can't handle return values.
|
||||
// So... best option is ...panic?
|
||||
let retval = unsafe { dvdcss_close(self.ptr) };
|
||||
assert!(retval == 0);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
[package]
|
||||
name = "kramer"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
[package]
|
||||
name = "mapping"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -1,14 +0,0 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
[package]
|
||||
name = "media"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -1,20 +0,0 @@
|
||||
use std::io::Read;
|
||||
|
||||
pub trait Media {
|
||||
fn type_(&self) -> MediaType;
|
||||
|
||||
fn encryption(&self) -> Vec<Box<dyn Encryption>>;
|
||||
|
||||
fn has_bus_encryption(&self) -> bool;
|
||||
|
||||
fn read(&self) -> Box<dyn Read>;
|
||||
}
|
||||
|
||||
pub enum MediaType {
|
||||
BluRay { writable: bool },
|
||||
DVD { writable: bool },
|
||||
}
|
||||
|
||||
pub trait Encryption {
|
||||
fn is_bus_encryption(&self) -> bool;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "plugins"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
semver.workspace = true
|
||||
@@ -1,180 +0,0 @@
|
||||
use semver::{Version, VersionReq};
|
||||
|
||||
pub struct PluginCompatBuilder {
|
||||
name: String,
|
||||
plugin_version: Version,
|
||||
core_version: Option<VersionReq>,
|
||||
is_preprocessor: bool,
|
||||
|
||||
dependencies: Vec<DependencyCompat>,
|
||||
incompatibilities: Vec<IncompatibilityCompat>,
|
||||
}
|
||||
|
||||
impl PluginCompatBuilder {
|
||||
// Fails if the provided name is empty.
|
||||
pub fn new(name: String, version: Version) -> Option<Self> {
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
name,
|
||||
plugin_version: version,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn core_version(mut self, version: VersionReq) -> Self {
|
||||
self.core_version = Some(version);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn preprocessor(mut self, is_preprocessor: bool) -> Self {
|
||||
self.is_preprocessor = is_preprocessor;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn dependency(mut self, dep: DependencyCompat) -> Self {
|
||||
self.dependencies.push(dep);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn incompatible(mut self, dep: IncompatibilityCompat) -> Self {
|
||||
self.incompatibilities.push(dep);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn finalize(self) -> PluginCompat {
|
||||
PluginCompat {
|
||||
name: self.name,
|
||||
plugin_version: self.plugin_version,
|
||||
core_version: self.core_version,
|
||||
is_preprocessor: self.is_preprocessor,
|
||||
dependencies: self.dependencies,
|
||||
incompatibilities: self.incompatibilities,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PluginCompatBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::from("AnonymousPlugin"),
|
||||
plugin_version: Version::new(0, 1, 0),
|
||||
core_version: None,
|
||||
is_preprocessor: false,
|
||||
dependencies: vec![],
|
||||
incompatibilities: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginCompat {
|
||||
name: String,
|
||||
plugin_version: Version,
|
||||
core_version: Option<VersionReq>,
|
||||
is_preprocessor: bool,
|
||||
|
||||
dependencies: Vec<DependencyCompat>,
|
||||
incompatibilities: Vec<IncompatibilityCompat>,
|
||||
}
|
||||
|
||||
// Most things shouldn't ever be modified by the caller, so passing
|
||||
// references to reduce memory consumption should be preferable.
|
||||
//
|
||||
// Worst case, maybe a few common calls on Version/VersionReq require
|
||||
// mutability, in which case maybe they won't be by reference.
|
||||
impl PluginCompat {
|
||||
pub fn new_dep_or_incompatible(name: String, version: Version) -> Self {
|
||||
let mut def = Self::default();
|
||||
def.name = name;
|
||||
def.plugin_version = version;
|
||||
def
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
pub fn plugin_version(&self) -> &Version {
|
||||
&self.plugin_version
|
||||
}
|
||||
|
||||
pub fn core_version(&self) -> Option<&VersionReq> {
|
||||
self.core_version.as_ref()
|
||||
}
|
||||
|
||||
pub fn is_preprocessor(&self) -> bool {
|
||||
self.is_preprocessor
|
||||
}
|
||||
|
||||
pub fn is_postprocessor(&self) -> bool {
|
||||
!self.is_preprocessor()
|
||||
}
|
||||
|
||||
pub fn dependencies(&self) -> &[DependencyCompat] {
|
||||
self.dependencies.as_slice()
|
||||
}
|
||||
|
||||
pub fn incompatibilities(&self) -> &[IncompatibilityCompat] {
|
||||
self.incompatibilities.as_slice()
|
||||
}
|
||||
|
||||
/// Returns if there is *any* incompatibility between `self` and `plugin`.
|
||||
pub fn incompatible_with<C: AsRef<PluginCompat>>(&self, plugin: C) -> bool {
|
||||
return self.contains_incompatibility_with(&plugin)
|
||||
&& plugin.as_ref().contains_incompatibility_with(self);
|
||||
}
|
||||
|
||||
/// Returns if `self` has any incompatibility with `plugin`
|
||||
fn contains_incompatibility_with<C: AsRef<PluginCompat>>(&self, plugin: C) -> bool {
|
||||
if !self.incompatibilities.is_empty()
|
||||
&& self.incompatibilities.iter().any(|inc| {
|
||||
inc.name() == plugin.as_ref().name()
|
||||
&& !inc.version.matches(&plugin.as_ref().plugin_version)
|
||||
})
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PluginCompat {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::from("AnonymousPlugin"),
|
||||
plugin_version: Version::new(0, 1, 0),
|
||||
core_version: None,
|
||||
is_preprocessor: false,
|
||||
dependencies: vec![],
|
||||
incompatibilities: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Self> for PluginCompat {
|
||||
fn as_ref(&self) -> &Self {
|
||||
&self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DependencyCompat {
|
||||
name: String,
|
||||
version: VersionReq,
|
||||
}
|
||||
|
||||
impl DependencyCompat {
|
||||
pub fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
pub fn plugin_version(&self) -> &VersionReq {
|
||||
&self.version
|
||||
}
|
||||
}
|
||||
|
||||
pub type IncompatibilityCompat = DependencyCompat;
|
||||
@@ -1,14 +0,0 @@
|
||||
use semver::Version;
|
||||
|
||||
use crate::compat::PluginCompat;
|
||||
|
||||
pub mod compat;
|
||||
|
||||
// Surely there's a way to automate this against the cargo manifest?
|
||||
pub const CORE_VERSION: Version = Version::new(0, 1, 0);
|
||||
|
||||
pub trait Plugin {
|
||||
fn name(&self) -> &str;
|
||||
|
||||
fn compatibility(&self) -> PluginCompat;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "scsi"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
derive_more.workspace = true
|
||||
nix = { version = "0.31.2", features = ["ioctl"] } # Re-exports a compatible libc version.
|
||||
packed_struct = "0.10"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.72.1"
|
||||
@@ -1,24 +0,0 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
// Path from which to search for shared libraries.
|
||||
// Is there no better (automated), platform-specific way?
|
||||
println!("cargo:rustc-link-search=/usr/lib");
|
||||
|
||||
// Locate and link the shared library.
|
||||
//println!("cargo:rustc-link-lib=sg3_utils");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/wrapper.h")
|
||||
// Invalidate the build if a header has changed.
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Failed to generate bindings.");
|
||||
|
||||
// Write the bindings to the $OUT_DIR/bindings.rs file.
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("bindings.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
use derive_more::{Display, From};
|
||||
use nix;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, From, Display)]
|
||||
pub enum Error {
|
||||
Nix(nix::Error),
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
mod error;
|
||||
mod sg;
|
||||
mod spc;
|
||||
|
||||
pub use error::Error;
|
||||
|
||||
use packed_struct::{
|
||||
derive::PackedStruct,
|
||||
types::{Integer, ReservedZero, bits::Bits},
|
||||
};
|
||||
|
||||
/*
|
||||
#[derive(Debug)]
|
||||
pub struct Command6 {
|
||||
opcode: u8,
|
||||
payload: [u8; 4],
|
||||
control: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Command10 {
|
||||
opcode: u8,
|
||||
payload: [u8; 8],
|
||||
control: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Command12 {
|
||||
opcode: u8,
|
||||
payload: [u8; 10],
|
||||
control: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Command16 {
|
||||
opcode: u8,
|
||||
payload: [u8; 14],
|
||||
control: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandVariable {
|
||||
opcode: u8,
|
||||
control: u8,
|
||||
payload: [u8; 4],
|
||||
additional_len: u8,
|
||||
service_action: [u8; 2],
|
||||
additional_payload: Vec<u8>,
|
||||
}
|
||||
|
||||
|
||||
// There's also XCDBs... but I'm not about to mess with that.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SenseData {}
|
||||
*/
|
||||
|
||||
#[derive(Debug, Default, PackedStruct)]
|
||||
#[packed_struct(bit_numbering = "msb0")]
|
||||
pub struct Control {
|
||||
#[packed_field(bits = "0..2")]
|
||||
vendor_specific: Integer<u8, Bits<2>>,
|
||||
#[packed_field(bits = "2..5")]
|
||||
_reserved: ReservedZero<Bits<3>>,
|
||||
#[packed_field(bits = "5..6")]
|
||||
naca: bool,
|
||||
#[packed_field(bits = "6")]
|
||||
_obsolete_1: bool, // SCSI uses LSB0 bit ordering, so name accordingly.
|
||||
#[packed_field(bits = "7")]
|
||||
_obsolete_0: bool, // SCSI uses LSB0 bit ordering, so name accordingly.
|
||||
}
|
||||
|
||||
pub trait Command {}
|
||||
@@ -1,85 +0,0 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(unused)]
|
||||
|
||||
use std::{fs::File, io::Write, ops::Add};
|
||||
|
||||
use packed_struct::{
|
||||
PackedStruct,
|
||||
derive::PackedStruct,
|
||||
types::{Integer, ReservedZero, bits::Bits},
|
||||
};
|
||||
|
||||
use crate::{Command, spc::InquiryCommand};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
|
||||
#[derive(PackedStruct)]
|
||||
#[packed_struct(endian = "msb")]
|
||||
pub struct SgCommand {
|
||||
#[packed_field(element_size_bytes = "36")]
|
||||
header: Header,
|
||||
#[packed_field(element_size_bytes = "6")]
|
||||
scsi_cmd: InquiryCommand,
|
||||
}
|
||||
|
||||
impl From<InquiryCommand> for SgCommand {
|
||||
fn from(value: InquiryCommand) -> Self {
|
||||
SgCommand {
|
||||
header: Header::new(&value, 0, 0),
|
||||
scsi_cmd: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PackedStruct)]
|
||||
#[packed_struct(endian = "msb")]
|
||||
pub struct Header {
|
||||
_packet_len: u32,
|
||||
|
||||
reply_len: u32,
|
||||
|
||||
#[packed_field(element_size_bits = "32")]
|
||||
_pack_id: ReservedZero<Bits<32>>,
|
||||
|
||||
result: u32,
|
||||
|
||||
#[packed_field(element_size_bits = "1")]
|
||||
twelve_byte: bool,
|
||||
#[packed_field(element_size_bits = "5")]
|
||||
target_status: Integer<u8, Bits<5>>,
|
||||
#[packed_field(element_size_bits = "8")]
|
||||
host_status: u8,
|
||||
#[packed_field(element_size_bits = "8")]
|
||||
driver_status: u8,
|
||||
#[packed_field(element_size_bits = "10")]
|
||||
_other_flags: ReservedZero<Bits<10>>,
|
||||
|
||||
sense_buffer: [u8; 16],
|
||||
}
|
||||
|
||||
impl Header {
|
||||
fn new(relevant_cmd: &InquiryCommand, in_size: u32, out_size: u32) -> Self {
|
||||
Self {
|
||||
_packet_len: (size_of::<Header>() as u32
|
||||
+ size_of::<InquiryCommand>() as u32
|
||||
+ in_size)
|
||||
.into(),
|
||||
reply_len: size_of::<Header>() as u32 + out_size,
|
||||
_pack_id: ReservedZero::default(),
|
||||
result: 0,
|
||||
twelve_byte: size_of_val(relevant_cmd) == 12,
|
||||
target_status: 0.into(),
|
||||
host_status: 0,
|
||||
driver_status: 0,
|
||||
_other_flags: ReservedZero::default(),
|
||||
|
||||
sense_buffer: [0; 16],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_command(mut fd: File, cmd: SgCommand) -> Result<(), std::io::Error> {
|
||||
fd.write_all(&cmd.pack().unwrap())
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
use packed_struct::derive::PackedStruct;
|
||||
use packed_struct::types::ReservedZero;
|
||||
use packed_struct::types::bits::Bits;
|
||||
|
||||
use crate::{Command, Control};
|
||||
|
||||
#[derive(Debug, PackedStruct)]
|
||||
#[packed_struct(endian = "msb")]
|
||||
pub struct InquiryCommand {
|
||||
opcode: u8,
|
||||
|
||||
#[packed_field(element_size_bits = "6")]
|
||||
_reserved: ReservedZero<Bits<6>>,
|
||||
#[packed_field(element_size_bits = "1")]
|
||||
_obsolete: ReservedZero<Bits<1>>,
|
||||
#[packed_field(element_size_bits = "1")]
|
||||
enable_vital_product_data: bool,
|
||||
|
||||
page_code: u8,
|
||||
|
||||
allocation_length: u16,
|
||||
|
||||
#[packed_field(element_size_bytes = "1")]
|
||||
control: Control,
|
||||
}
|
||||
|
||||
impl InquiryCommand {
|
||||
pub fn new(evpd: bool, page_code: u8, alloc_len: u16) -> Self {
|
||||
let mut cmd = Self::default();
|
||||
cmd.enable_vital_product_data = evpd;
|
||||
cmd.page_code = page_code;
|
||||
cmd.allocation_length = alloc_len;
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InquiryCommand {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
opcode: 0x12,
|
||||
_reserved: ReservedZero::default(),
|
||||
_obsolete: ReservedZero::default(),
|
||||
enable_vital_product_data: false,
|
||||
page_code: 0,
|
||||
allocation_length: 0,
|
||||
control: Control::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Command for InquiryCommand {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::OpenOptions;
|
||||
|
||||
use crate::sg::{SgCommand, send_command};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pack_inquiry() {
|
||||
let cmd = InquiryCommand::new(false, 0, 36);
|
||||
println!("{}", cmd.packed_struct_display_formatter());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_inquiry() {
|
||||
let fd = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/sr0")
|
||||
.unwrap();
|
||||
dbg!(&fd);
|
||||
|
||||
let cmd = SgCommand::from(InquiryCommand::new(false, 0, 36));
|
||||
|
||||
send_command(fd, cmd).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
#include <scsi/sg.h>
|
||||
Vendored
-1
Submodule external/libdvdcss deleted from 2682a4a7ed
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "scraper"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
plugins.workspace = true
|
||||
|
||||
semver.workspace = true
|
||||
@@ -1,31 +0,0 @@
|
||||
use plugins::compat::{PluginCompat, PluginCompatBuilder};
|
||||
use plugins::{self, Plugin};
|
||||
use semver::{Version, VersionReq};
|
||||
|
||||
const PLUGIN_NAME: &str = "Scraper";
|
||||
const PLUGIN_VERSION: Version = Version::new(0, 1, 0);
|
||||
|
||||
pub struct DynamicPlugin;
|
||||
|
||||
impl DynamicPlugin {
|
||||
#[allow(dead_code)]
|
||||
#[unsafe(no_mangle)]
|
||||
fn new() -> Box<dyn Plugin> {
|
||||
Box::new(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for DynamicPlugin {
|
||||
#[unsafe(no_mangle)]
|
||||
fn name(&self) -> &str {
|
||||
PLUGIN_NAME
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
fn compatibility(&self) -> PluginCompat {
|
||||
PluginCompatBuilder::new(PLUGIN_NAME.to_string(), PLUGIN_VERSION)
|
||||
.unwrap()
|
||||
.core_version(VersionReq::parse(plugins::CORE_VERSION.to_string().as_str()).unwrap())
|
||||
.finalize()
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
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
@@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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::*;
|
||||
}
|
||||
@@ -0,0 +1,859 @@
|
||||
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| {
|
||||
dbg!(mc.stage);
|
||||
dbg!(stage);
|
||||
dbg!(mc.stage == stage);
|
||||
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()
|
||||
});
|
||||
dbg!(&self.map);
|
||||
Some(dbg!(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: 1 }
|
||||
);
|
||||
*/
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#![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;
|
||||
@@ -0,0 +1,6 @@
|
||||
#![allow(unused_imports)]
|
||||
|
||||
pub use super::cluster::Cluster;
|
||||
pub use super::domain::{Domain, DomainOverlap};
|
||||
pub use super::map::MapFile;
|
||||
pub use super::stage::Stage;
|
||||
@@ -0,0 +1,22 @@
|
||||
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: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
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()
|
||||
});
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
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 {
|
||||
//dbg!(&self.map.map);
|
||||
//self.map.defrag();
|
||||
//dbg!(&self.map.map);
|
||||
|
||||
match self.map.get_stage() {
|
||||
Stage::Patchwork { depth } => self.copy_patchwork(dbg!(depth))?,
|
||||
Stage::Isolate => todo!(),
|
||||
Stage::BruteForceAndDesperation => todo!(),
|
||||
Stage::Damaged | Stage::Intact => {
|
||||
println!("Cannot recover further.");
|
||||
|
||||
is_finished = true
|
||||
}
|
||||
};
|
||||
|
||||
dbg!(self.input.stream_position());
|
||||
|
||||
// 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 }) {
|
||||
dbg!();
|
||||
// Order of these two expressions matters, stupid.
|
||||
buf_capacity /= depth + 1;
|
||||
|
||||
for cluster in self.map.get_clusters(Stage::Patchwork { depth }) {
|
||||
dbg!();
|
||||
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);
|
||||
dbg!(&self.map.map);
|
||||
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
@@ -0,0 +1,83 @@
|
||||
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(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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