feat: Guará 0.1.0 — recuperação de arquivos (núcleo, CLI e interface gráfica)
Núcleo em Rust somente leitura: NTFS, FAT, exFAT, partições MBR/GPT e perdidas, carving por estrutura, fusão sem duplicatas, confiança por arquivo, imagem forense, pausar/retomar. CLI `guara`, UI web/Tauri, CI e releases por tag. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
765cb0d4a2
57 files changed
+8621
No files matched your search
@@ -0,0 +1,249 @@
|
||||
//! Enumeração de dispositivos de armazenamento (melhor esforço por plataforma).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceInfo {
|
||||
pub path: String,
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
/// "HD", "SSD", "NVMe", "USB", "Disco" ou "Imagem".
|
||||
pub kind: String,
|
||||
pub removable: bool,
|
||||
pub model: Option<String>,
|
||||
pub mounted_at: Vec<String>,
|
||||
/// O processo atual consegue abrir o dispositivo para leitura bruta.
|
||||
pub readable: bool,
|
||||
/// SSD com descarte (TRIM) ativo: arquivos apagados tendem a sumir de verdade.
|
||||
pub trim: Option<bool>,
|
||||
}
|
||||
|
||||
/// (dispositivo, ponto de montagem)
|
||||
pub fn mount_table() -> Vec<(String, String)> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let Ok(s) = std::fs::read_to_string("/proc/mounts") else {
|
||||
return vec![];
|
||||
};
|
||||
s.lines()
|
||||
.filter_map(|l| {
|
||||
let mut it = l.split_whitespace();
|
||||
let d = it.next()?;
|
||||
let m = it.next()?;
|
||||
d.starts_with("/dev/")
|
||||
.then(|| (d.to_string(), m.replace("\\040", " ")))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let Ok(o) = std::process::Command::new("mount").output() else {
|
||||
return vec![];
|
||||
};
|
||||
let mut v = vec![];
|
||||
for l in String::from_utf8_lossy(&o.stdout).lines() {
|
||||
if let Some((d, rest)) = l.split_once(" on ") {
|
||||
if d.starts_with("/dev/") {
|
||||
let m = rest.split(" (").next().unwrap_or("").to_string();
|
||||
v.push((d.to_string(), m.clone()));
|
||||
v.push((d.replace("/dev/disk", "/dev/rdisk"), m));
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_devices() -> Vec<DeviceInfo> {
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux();
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos();
|
||||
#[cfg(windows)]
|
||||
return windows();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux() -> Vec<DeviceInfo> {
|
||||
use std::fs;
|
||||
let mounts = mount_table();
|
||||
let mut out = vec![];
|
||||
let Ok(rd) = fs::read_dir("/sys/block") else {
|
||||
return out;
|
||||
};
|
||||
for e in rd.flatten() {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
if ["loop", "ram", "zram", "dm-", "md", "sr", "fd", "nbd"]
|
||||
.iter()
|
||||
.any(|p| name.starts_with(p))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let base = e.path();
|
||||
let rd_str = |p: &str| {
|
||||
fs::read_to_string(base.join(p))
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
};
|
||||
let sectors: u64 = rd_str("size").and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
if sectors == 0 {
|
||||
continue;
|
||||
}
|
||||
let removable = rd_str("removable").as_deref() == Some("1");
|
||||
let rotational = rd_str("queue/rotational").as_deref() == Some("1");
|
||||
let trim = rd_str("queue/discard_max_bytes")
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.map(|v| v > 0);
|
||||
let path = format!("/dev/{name}");
|
||||
let kind = if removable {
|
||||
"USB"
|
||||
} else if name.starts_with("nvme") {
|
||||
"NVMe"
|
||||
} else if rotational {
|
||||
"HD"
|
||||
} else {
|
||||
"SSD"
|
||||
};
|
||||
let model = rd_str("device/model").or_else(|| rd_str("device/name"));
|
||||
let mounted_at = mounts
|
||||
.iter()
|
||||
.filter(|(d, _)| {
|
||||
d.starts_with(&path)
|
||||
&& d[path.len()..]
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_digit() || c == 'p')
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.map(|(_, m)| m.clone())
|
||||
.collect();
|
||||
let readable = fs::File::open(&path).is_ok();
|
||||
out.push(DeviceInfo {
|
||||
path,
|
||||
name: model.clone().unwrap_or_else(|| name.clone()),
|
||||
size: sectors * 512,
|
||||
kind: kind.into(),
|
||||
removable,
|
||||
model,
|
||||
mounted_at,
|
||||
readable,
|
||||
trim: if rotational { None } else { trim },
|
||||
});
|
||||
}
|
||||
out.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos() -> Vec<DeviceInfo> {
|
||||
let mut out = vec![];
|
||||
let Ok(rd) = std::fs::read_dir("/dev") else {
|
||||
return out;
|
||||
};
|
||||
let mounts = mount_table();
|
||||
for e in rd.flatten() {
|
||||
let n = e.file_name().to_string_lossy().to_string();
|
||||
let Some(num) = n.strip_prefix("disk") else {
|
||||
continue;
|
||||
};
|
||||
if num.is_empty() || !num.chars().all(|c| c.is_ascii_digit()) {
|
||||
continue;
|
||||
}
|
||||
let info = std::process::Command::new("diskutil")
|
||||
.args(["info", &n])
|
||||
.output()
|
||||
.ok();
|
||||
let text = info
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||
.unwrap_or_default();
|
||||
let field = |k: &str| {
|
||||
text.lines().find_map(|l| {
|
||||
l.trim()
|
||||
.strip_prefix(k)
|
||||
.map(|v| v.trim().trim_start_matches(':').trim().to_string())
|
||||
})
|
||||
};
|
||||
let size = field("Disk Size:")
|
||||
.and_then(|v| {
|
||||
v.split('(')
|
||||
.nth(1)
|
||||
.and_then(|s| s.split_whitespace().next().map(String::from))
|
||||
})
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let ssd = field("Solid State:").map(|v| v == "Yes").unwrap_or(false);
|
||||
let removable = field("Removable Media:")
|
||||
.map(|v| v != "Fixed" && v != "No")
|
||||
.unwrap_or(false)
|
||||
|| field("Protocol:").map(|v| v == "USB").unwrap_or(false);
|
||||
let model = field("Device / Media Name:");
|
||||
let path = format!("/dev/r{n}");
|
||||
let readable = std::fs::File::open(&path).is_ok();
|
||||
let mounted_at = mounts
|
||||
.iter()
|
||||
.filter(|(d, _)| d.starts_with(&format!("/dev/{n}")))
|
||||
.map(|(_, m)| m.clone())
|
||||
.collect();
|
||||
out.push(DeviceInfo {
|
||||
path,
|
||||
name: model.clone().unwrap_or(n.clone()),
|
||||
size,
|
||||
kind: if removable {
|
||||
"USB"
|
||||
} else if ssd {
|
||||
"SSD"
|
||||
} else {
|
||||
"HD"
|
||||
}
|
||||
.into(),
|
||||
removable,
|
||||
model,
|
||||
mounted_at,
|
||||
readable,
|
||||
trim: ssd.then_some(true),
|
||||
});
|
||||
}
|
||||
out.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows() -> Vec<DeviceInfo> {
|
||||
let mut out = vec![];
|
||||
for i in 0..32 {
|
||||
let path = format!(r"\\.\PhysicalDrive{i}");
|
||||
match crate::source::open_windows_device(std::path::Path::new(&path)) {
|
||||
Ok(s) => {
|
||||
use crate::source::Source;
|
||||
out.push(DeviceInfo {
|
||||
path,
|
||||
name: format!("Disco {i}"),
|
||||
size: s.size(),
|
||||
kind: "Disco".into(),
|
||||
removable: false,
|
||||
model: None,
|
||||
mounted_at: vec![],
|
||||
readable: true,
|
||||
trim: None,
|
||||
});
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => out.push(DeviceInfo {
|
||||
path,
|
||||
name: format!("Disco {i}"),
|
||||
size: 0,
|
||||
kind: "Disco".into(),
|
||||
removable: false,
|
||||
model: None,
|
||||
mounted_at: vec![],
|
||||
readable: false,
|
||||
trim: None,
|
||||
}),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
Reference in new issue
Block a user