Compare commits

1 Commits

Author SHA1 Message Date
Cutieguwu 6190e4d968 stuff?... Probably borked after rebase. 2026-06-21 11:12:37 -04:00
45 changed files with 3498 additions and 954 deletions
+1 -2
View File
@@ -1,2 +1 @@
**/target
**/*.lock
/target
-3
View File
@@ -1,3 +0,0 @@
[submodule "external/libdvdcss"]
path = external/libdvdcss
url = https://code.videolan.org/videolan/libdvdcss.git
Generated
+1783
View File
File diff suppressed because it is too large Load Diff
+21 -31
View File
@@ -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"
+48 -49
View File
@@ -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.
-12
View File
@@ -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"
-3
View File
@@ -1,3 +0,0 @@
# dvdcss-sys
A crate for pure bindings against the system libdvdcss.
-33
View File
@@ -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!");
}
-6
View File
@@ -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"));
-2
View File
@@ -1,2 +0,0 @@
#include "dvdcss/dvdcss.h"
#include "libdvdcss.h"
-17
View File
@@ -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 = []
-11
View File
@@ -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.
-180
View File
@@ -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);
}
}
-10
View File
@@ -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]
-3
View File
@@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}
-10
View File
@@ -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]
-14
View File
@@ -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);
}
}
-10
View File
@@ -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]
-20
View File
@@ -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;
}
-11
View File
@@ -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
-180
View File
@@ -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;
-14
View File
@@ -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;
}
-16
View File
@@ -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"
-24
View File
@@ -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!");
}
-9
View File
@@ -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),
}
-73
View File
@@ -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 {}
-85
View File
@@ -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())
}
-80
View File
@@ -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
View File
@@ -1 +0,0 @@
#include <scsi/sg.h>
Submodule external/libdvdcss deleted from 2682a4a7ed
-13
View File
@@ -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
-31
View File
@@ -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
View File
@@ -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,
}
+154
View File
@@ -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
View File
@@ -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
}
+62
View File
@@ -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()
}
+68
View File
@@ -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::*;
}
+859
View File
@@ -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
)
}
}
+12
View File
@@ -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;
+6
View File
@@ -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;
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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(),
_ => (),
}
}
}
+17
View File
@@ -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::*;