feat: pré-visualização completa, tema, nova identidade e correções (0.2.0)
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
- Preview de imagem, vídeo/MP4, áudio, PDF (pdf.js), texto, Office/ODF, ZIP e hex, lido sob demanda com Range; tela cheia com navegação por teclado. - Seletor de tema Auto/Claro/Escuro persistido (vale também como administrador). - Identidade verde-azulada com logo de pixels que formam um lobo. - Corrige reinício como administrador no AppImage (usa $APPIMAGE) e mostra o motivo. - Corrige barra inferior piscando (render idempotente, barra criada uma vez). - Corrige FAT32 em mídias pequenas não reconhecido. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
510f1bc48e
commit
c5c94a9956
32 files changed
+1666
-227
No files matched your search
@@ -12,6 +12,7 @@ pub mod partition;
|
||||
pub mod recover;
|
||||
pub mod scan;
|
||||
pub mod service;
|
||||
pub mod settings;
|
||||
pub mod signatures;
|
||||
pub mod source;
|
||||
pub mod types;
|
||||
|
||||
@@ -92,17 +92,16 @@ fn detect_fat(b: &[u8]) -> Option<(FsKind, Option<u64>)> {
|
||||
let root_secs = (root_entries * 32).div_ceil(bps);
|
||||
let data = total.saturating_sub(reserved + nfats * fatsz + root_secs);
|
||||
let clusters = data / spc;
|
||||
let kind = if clusters < 4085 {
|
||||
FsKind::Fat12
|
||||
} else if clusters < 65525 {
|
||||
FsKind::Fat16
|
||||
} else {
|
||||
// O layout do BPB manda: raiz fixa zerada + FAT de 32 bits = FAT32, mesmo com poucos
|
||||
// clusters (mkfs.vfat -F 32 em mídias pequenas gera isso e o Linux monta).
|
||||
let is32 = root_entries == 0 && fat16 == 0 && fat32 != 0;
|
||||
let kind = if is32 {
|
||||
FsKind::Fat32
|
||||
} else if clusters < 4085 {
|
||||
FsKind::Fat12
|
||||
} else {
|
||||
FsKind::Fat16
|
||||
};
|
||||
// Coerência: FAT32 tem root_entries == 0 e fat16 == 0.
|
||||
if (kind == FsKind::Fat32) != (root_entries == 0 && fat16 == 0) {
|
||||
return None;
|
||||
}
|
||||
Some((kind, Some(total * bps)))
|
||||
}
|
||||
|
||||
|
||||
@@ -210,3 +210,42 @@ pub fn write_file(src: &dyn Source, f: &Found, path: &Path) -> Result<String> {
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Lê `buf.len()` bytes do conteúdo lógico de um achado a partir de `offset`
|
||||
/// (usado para streaming/pré-visualização sem gravar em disco).
|
||||
pub fn read_found_at(
|
||||
src: &dyn Source,
|
||||
f: &Found,
|
||||
offset: u64,
|
||||
buf: &mut [u8],
|
||||
) -> std::io::Result<usize> {
|
||||
if offset >= f.size || buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let want = buf.len().min((f.size - offset) as usize);
|
||||
match &f.data {
|
||||
DataLoc::Resident { bytes } => {
|
||||
let o = offset as usize;
|
||||
let n = want.min(bytes.len().saturating_sub(o));
|
||||
buf[..n].copy_from_slice(&bytes[o..o + n]);
|
||||
Ok(n)
|
||||
}
|
||||
DataLoc::Extents { extents } => {
|
||||
let mut pos = 0u64;
|
||||
for e in extents {
|
||||
let elen = e.len.min(f.size.saturating_sub(pos));
|
||||
if offset < pos + elen {
|
||||
let within = offset - pos;
|
||||
let n = want.min((elen - within) as usize);
|
||||
if e.sparse {
|
||||
buf[..n].fill(0);
|
||||
return Ok(n);
|
||||
}
|
||||
return src.read_at(e.offset + within, &mut buf[..n]);
|
||||
}
|
||||
pos += elen;
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,37 +278,18 @@ impl Engine {
|
||||
Ok(json!({ "total": total, "items": page }))
|
||||
}
|
||||
|
||||
pub fn preview(&self, id: &str, file: u64) -> Result<Value> {
|
||||
/// Achado + origem para leitura posicional (streaming de pré-visualização).
|
||||
pub fn open_file(&self, id: &str, file: u64) -> Result<(Found, Arc<dyn Source>)> {
|
||||
let j = self.job(id)?;
|
||||
let (f, src) = {
|
||||
let s = j.st.lock().unwrap();
|
||||
let i = *s
|
||||
.index
|
||||
.get(&file)
|
||||
.ok_or_else(|| Error::msg("arquivo não encontrado"))?;
|
||||
(
|
||||
s.found[i].clone(),
|
||||
j.src.clone().ok_or_else(|| Error::msg("sem origem"))?,
|
||||
)
|
||||
};
|
||||
let mime = match f.ext.as_str() {
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"png" => "image/png",
|
||||
"gif" => "image/gif",
|
||||
"webp" => "image/webp",
|
||||
"bmp" => "image/bmp",
|
||||
_ => return Ok(json!({ "mime": null })),
|
||||
};
|
||||
if f.size > 8 * 1024 * 1024 {
|
||||
return Ok(json!({ "mime": null, "reason": "grande demais para pré-visualizar" }));
|
||||
}
|
||||
let tmp =
|
||||
std::env::temp_dir().join(format!("guara-preview-{}-{}", std::process::id(), f.id));
|
||||
let r = recover::write_file(src.as_ref(), &f, &tmp);
|
||||
let bytes = std::fs::read(&tmp);
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
r?;
|
||||
Ok(json!({ "mime": mime, "base64": base64(&bytes?) }))
|
||||
let s = j.st.lock().unwrap();
|
||||
let i = *s
|
||||
.index
|
||||
.get(&file)
|
||||
.ok_or_else(|| Error::msg("arquivo não encontrado"))?;
|
||||
Ok((
|
||||
s.found[i].clone(),
|
||||
j.src.clone().ok_or_else(|| Error::msg("sem origem"))?,
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- recuperação
|
||||
@@ -534,12 +515,12 @@ impl Engine {
|
||||
.unwrap_or_default();
|
||||
self.results(&s("job")?, &q)?
|
||||
}
|
||||
"preview" => self.preview(
|
||||
&s("job")?,
|
||||
p.get("id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| Error::msg("id ausente"))?,
|
||||
)?,
|
||||
"settings_get" => json!(crate::settings::load()),
|
||||
"settings_set" => {
|
||||
let st: crate::settings::Settings = serde_json::from_value(p.clone())?;
|
||||
crate::settings::save(&st)?;
|
||||
json!({})
|
||||
}
|
||||
"recover_start" => {
|
||||
let ids: Option<Vec<u64>> = p
|
||||
.get("ids")
|
||||
@@ -697,3 +678,175 @@ pub fn base64(data: &[u8]) -> String {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Tipo MIME para servir a pré-visualização. Formatos que poderiam executar
|
||||
/// script (HTML/SVG/XML) saem como texto puro.
|
||||
pub fn mime_for(ext: &str) -> &'static str {
|
||||
match ext.to_ascii_lowercase().as_str() {
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"png" => "image/png",
|
||||
"gif" => "image/gif",
|
||||
"webp" => "image/webp",
|
||||
"bmp" => "image/bmp",
|
||||
"avif" => "image/avif",
|
||||
"ico" => "image/x-icon",
|
||||
"mp4" | "m4v" => "video/mp4",
|
||||
"mov" => "video/quicktime",
|
||||
"webm" => "video/webm",
|
||||
"mkv" => "video/x-matroska",
|
||||
"mp3" => "audio/mpeg",
|
||||
"wav" => "audio/wav",
|
||||
"ogg" | "opus" => "audio/ogg",
|
||||
"m4a" => "audio/mp4",
|
||||
"aac" => "audio/aac",
|
||||
"flac" => "audio/flac",
|
||||
"pdf" => "application/pdf",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
||||
/// Trecho de um arquivo achado, pronto para responder a uma requisição HTTP
|
||||
/// (com suporte a `Range`) tanto no modo `guara ui` quanto no app Tauri.
|
||||
pub struct FileSlice {
|
||||
found: Found,
|
||||
src: Arc<dyn Source>,
|
||||
pub start: u64,
|
||||
/// Inclusivo.
|
||||
pub end: u64,
|
||||
pub total: u64,
|
||||
pub partial: bool,
|
||||
}
|
||||
|
||||
impl FileSlice {
|
||||
pub fn len(&self) -> u64 {
|
||||
if self.total == 0 {
|
||||
0
|
||||
} else {
|
||||
self.end - self.start + 1
|
||||
}
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
pub fn status(&self) -> u16 {
|
||||
if self.partial {
|
||||
206
|
||||
} else {
|
||||
200
|
||||
}
|
||||
}
|
||||
pub fn headers(&self) -> Vec<(&'static str, String)> {
|
||||
let mut h = vec![
|
||||
("Content-Type", mime_for(&self.found.ext).to_string()),
|
||||
("Accept-Ranges", "bytes".into()),
|
||||
("Cache-Control", "no-store".into()),
|
||||
("X-Content-Type-Options", "nosniff".into()),
|
||||
// Conteúdo recuperado é não confiável: nunca executa script nem carrega nada.
|
||||
(
|
||||
"Content-Security-Policy",
|
||||
"sandbox; default-src 'none'".into(),
|
||||
),
|
||||
("Access-Control-Allow-Origin", "*".into()),
|
||||
("Content-Disposition", "inline".into()),
|
||||
];
|
||||
if self.partial {
|
||||
h.push((
|
||||
"Content-Range",
|
||||
format!("bytes {}-{}/{}", self.start, self.end, self.total),
|
||||
));
|
||||
}
|
||||
h
|
||||
}
|
||||
/// Lê o trecho inteiro em memória (limitado a `cap` bytes).
|
||||
pub fn read_all(&self, cap: u64) -> std::io::Result<Vec<u8>> {
|
||||
let n = self.len().min(cap) as usize;
|
||||
let mut out = vec![0u8; n];
|
||||
let mut got = 0;
|
||||
while got < n {
|
||||
let r = crate::recover::read_found_at(
|
||||
self.src.as_ref(),
|
||||
&self.found,
|
||||
self.start + got as u64,
|
||||
&mut out[got..],
|
||||
)?;
|
||||
if r == 0 {
|
||||
break;
|
||||
}
|
||||
got += r;
|
||||
}
|
||||
out.truncate(got);
|
||||
Ok(out)
|
||||
}
|
||||
pub fn reader(self) -> FileSliceReader {
|
||||
FileSliceReader {
|
||||
pos: self.start,
|
||||
slice: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileSliceReader {
|
||||
slice: FileSlice,
|
||||
pos: u64,
|
||||
}
|
||||
|
||||
impl std::io::Read for FileSliceReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
if self.pos > self.slice.end || self.slice.total == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let left = (self.slice.end - self.pos + 1) as usize;
|
||||
let n = buf.len().min(left).min(1 << 20);
|
||||
let r = crate::recover::read_found_at(
|
||||
self.slice.src.as_ref(),
|
||||
&self.slice.found,
|
||||
self.pos,
|
||||
&mut buf[..n],
|
||||
)?;
|
||||
self.pos += r as u64;
|
||||
Ok(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
/// Resolve `/file/<job>/<id>` + cabeçalho Range. `Err(status)` em caso de falha.
|
||||
pub fn file_slice(
|
||||
&self,
|
||||
job: &str,
|
||||
id: u64,
|
||||
range: Option<&str>,
|
||||
) -> std::result::Result<FileSlice, u16> {
|
||||
let (found, src) = self.open_file(job, id).map_err(|_| 404u16)?;
|
||||
let total = found.size;
|
||||
let (mut start, mut end, mut partial) = (0u64, total.saturating_sub(1), false);
|
||||
if let Some(spec) = range.and_then(|r| r.strip_prefix("bytes=")) {
|
||||
let spec = spec.split(',').next().unwrap_or("");
|
||||
if let Some((a, b)) = spec.split_once('-') {
|
||||
let parsed = match (a.trim().parse::<u64>(), b.trim().parse::<u64>()) {
|
||||
(Ok(a), Ok(b)) => Some((a, b.min(total.saturating_sub(1)))),
|
||||
(Ok(a), Err(_)) if b.trim().is_empty() => Some((a, total.saturating_sub(1))),
|
||||
(Err(_), Ok(n)) if a.trim().is_empty() && n > 0 => {
|
||||
Some((total.saturating_sub(n), total.saturating_sub(1)))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some((a, b)) = parsed {
|
||||
if a >= total || a > b {
|
||||
return Err(416);
|
||||
}
|
||||
start = a;
|
||||
end = b;
|
||||
partial = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(FileSlice {
|
||||
found,
|
||||
src,
|
||||
start,
|
||||
end,
|
||||
total,
|
||||
partial,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Preferências do usuário (tema etc.), compartilhadas entre o app normal e o
|
||||
//! reiniciado como administrador.
|
||||
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
/// "auto", "light" ou "dark".
|
||||
pub theme: String,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: "auto".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pasta de configuração do usuário *real*, mesmo quando rodando como root
|
||||
/// via sudo/pkexec (para o tema não "mudar" ao elevar privilégios).
|
||||
pub fn config_dir() -> PathBuf {
|
||||
if let Ok(d) = std::env::var("GUARA_CONFIG_DIR") {
|
||||
if !d.is_empty() {
|
||||
return PathBuf::from(d);
|
||||
}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if let Some(home) = invoking_user_home() {
|
||||
return default_dir(home);
|
||||
}
|
||||
}
|
||||
default_dir(PathBuf::from(
|
||||
std::env::var("HOME")
|
||||
.or_else(|_| std::env::var("USERPROFILE"))
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
|
||||
fn default_dir(home: PathBuf) -> PathBuf {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Ok(a) = std::env::var("APPDATA") {
|
||||
return PathBuf::from(a).join("Guara");
|
||||
}
|
||||
home.join("AppData").join("Roaming").join("Guara")
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
home.join("Library")
|
||||
.join("Application Support")
|
||||
.join("Guara")
|
||||
}
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
{
|
||||
match std::env::var("XDG_CONFIG_HOME") {
|
||||
Ok(x)
|
||||
if !x.is_empty()
|
||||
&& std::env::var("SUDO_USER").is_err()
|
||||
&& std::env::var("PKEXEC_UID").is_err() =>
|
||||
{
|
||||
PathBuf::from(x).join("guara")
|
||||
}
|
||||
_ => home.join(".config").join("guara"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Se somos root por sudo/pkexec, a home de quem invocou.
|
||||
#[cfg(unix)]
|
||||
fn invoking_user_home() -> Option<PathBuf> {
|
||||
// SAFETY: geteuid não tem pré-condições.
|
||||
if unsafe { libc::geteuid() } != 0 {
|
||||
return None;
|
||||
}
|
||||
let user = std::env::var("SUDO_USER").ok().filter(|u| u != "root");
|
||||
let uid = std::env::var("PKEXEC_UID").ok();
|
||||
let passwd = std::fs::read_to_string("/etc/passwd").ok()?;
|
||||
for l in passwd.lines() {
|
||||
let f: Vec<&str> = l.split(':').collect();
|
||||
if f.len() >= 6 && (user.as_deref() == Some(f[0]) || uid.as_deref() == Some(f[2])) {
|
||||
return Some(PathBuf::from(f[5]));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn file() -> PathBuf {
|
||||
config_dir().join("settings.json")
|
||||
}
|
||||
|
||||
pub fn load() -> Settings {
|
||||
std::fs::read(file())
|
||||
.ok()
|
||||
.and_then(|b| serde_json::from_slice(&b).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn save(s: &Settings) -> Result<()> {
|
||||
let dir = config_dir();
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let path = file();
|
||||
std::fs::write(&path, serde_json::to_vec_pretty(s)?)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// Como root, devolve a posse ao dono da pasta para o app normal conseguir regravar.
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
if unsafe { libc::geteuid() } == 0 {
|
||||
if let Ok(m) = std::fs::metadata(&dir) {
|
||||
let _ = std::os::unix::fs::chown(&path, Some(m.uid()), Some(m.gid()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -231,3 +231,74 @@ fn pausa_e_retoma_encontra_o_mesmo() {
|
||||
b.sort();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
/// Streaming de pré-visualização: Range devolve exatamente os bytes do arquivo recuperado.
|
||||
#[test]
|
||||
fn file_slice_com_range_bate_com_o_arquivo_recuperado() {
|
||||
use std::io::Read;
|
||||
let Some(dir) = fixtures() else { return };
|
||||
let path = dir.join("ntfs.img");
|
||||
let eng = service::Engine::new();
|
||||
let job = eng
|
||||
.start_scan(
|
||||
path.to_str().unwrap(),
|
||||
ScanOptions {
|
||||
mode: ScanMode::Quick,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
loop {
|
||||
let st = eng.status(&job).unwrap();
|
||||
if st["status"] != "running" {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
let res = eng
|
||||
.results(
|
||||
&job,
|
||||
&service::Query {
|
||||
query: Some("relatorio.pdf".into()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let id = res["items"][0]["id"].as_u64().unwrap();
|
||||
let size = res["items"][0]["size"].as_u64().unwrap();
|
||||
let want = expected(&dir, "ntfs")
|
||||
.into_iter()
|
||||
.find(|(_, p)| p.ends_with("relatorio.pdf"))
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
let full = eng.file_slice(&job, id, None).unwrap();
|
||||
assert_eq!((full.status(), full.len()), (200, size));
|
||||
let mut all = vec![];
|
||||
full.reader().read_to_end(&mut all).unwrap();
|
||||
assert_eq!(sha256(&all), want);
|
||||
|
||||
let part = eng.file_slice(&job, id, Some("bytes=100-199")).unwrap();
|
||||
assert_eq!((part.status(), part.len()), (206, 100));
|
||||
assert_eq!(part.read_all(1 << 20).unwrap(), all[100..200]);
|
||||
let tail = eng.file_slice(&job, id, Some("bytes=-50")).unwrap();
|
||||
assert_eq!(tail.read_all(1 << 20).unwrap(), all[all.len() - 50..]);
|
||||
assert_eq!(
|
||||
eng.file_slice(&job, id, Some(&format!("bytes={size}-")))
|
||||
.err(),
|
||||
Some(416)
|
||||
);
|
||||
assert_eq!(eng.file_slice(&job, 999_999, None).err(), Some(404));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuracoes_de_tema_persistem() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::env::set_var("GUARA_CONFIG_DIR", dir.path());
|
||||
assert_eq!(settings::load().theme, "auto");
|
||||
settings::save(&settings::Settings {
|
||||
theme: "dark".into(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(settings::load().theme, "dark");
|
||||
}
|
||||
@@ -86,3 +86,58 @@ fn varredura_rapida_nao_faz_carving() {
|
||||
7
|
||||
);
|
||||
}
|
||||
|
||||
/// Regressão: mkfs.vfat -F 32 em mídia pequena (< 65525 clusters) é FAT32 pelo BPB.
|
||||
#[test]
|
||||
fn fat32_pequeno_com_poucos_clusters_e_reconhecido() {
|
||||
use std::process::Command;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let img = dir.path().join("p.img");
|
||||
let ok = Command::new("truncate")
|
||||
.args(["-s", "32M"])
|
||||
.arg(&img)
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
&& Command::new("mkfs.vfat")
|
||||
.args(["-F", "32"])
|
||||
.arg(&img)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
let file = dir.path().join("arquivo.txt");
|
||||
std::fs::write(&file, b"conteudo de teste\n".repeat(500)).unwrap();
|
||||
let ok = ok
|
||||
&& Command::new("mcopy")
|
||||
.arg("-i")
|
||||
.arg(&img)
|
||||
.arg(&file)
|
||||
.arg("::/")
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
&& Command::new("mdel")
|
||||
.arg("-i")
|
||||
.arg(&img)
|
||||
.arg("::/arquivo.txt")
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
if !ok {
|
||||
eprintln!("SKIP: mtools/dosfstools ausentes");
|
||||
return;
|
||||
}
|
||||
let res = scan_path(
|
||||
&img,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Quick,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let layout = res.layout.unwrap();
|
||||
assert_eq!(layout.partitions[0].fs, FsKind::Fat32);
|
||||
assert_eq!(res.found.len(), 1);
|
||||
let out = tempfile::tempdir().unwrap();
|
||||
recover_all(&img, &res.found, out.path());
|
||||
assert!(hashes_in(out.path()).contains_key(&sha256(&b"conteudo de teste\n".repeat(500))));
|
||||
}
|
||||
Reference in new issue
Block a user