From beb83fef92895eae5f399c99a99b13739d00cd3a Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Mon, 19 Oct 2026 11:41:58 -0300 Subject: [PATCH] =?UTF-8?q?Formata=C3=A7=C3=A3o=20(cargo=20fmt)=20e=20ajus?= =?UTF-8?q?tes=20do=20clippy=20para=20o=20CI=20da=201.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- crates/guara-cli/src/main.rs | 56 ++++- crates/guara-core/src/aff4.rs | 218 +++++++++++++---- crates/guara-core/src/assemble.rs | 178 +++++++++++--- crates/guara-core/src/crypt/bitlocker.rs | 89 +++++-- crates/guara-core/src/crypt/filevault.rs | 50 +++- crates/guara-core/src/crypt/luks.rs | 102 ++++++-- crates/guara-core/src/crypt/mod.rs | 31 ++- crates/guara-core/src/defrag.rs | 108 +++++++-- crates/guara-core/src/ewf.rs | 146 ++++++++--- crates/guara-core/src/formats.rs | 295 +++++++++++++++++++---- crates/guara-core/src/fs/apfs.rs | 204 ++++++++++++---- crates/guara-core/src/fs/btrfs.rs | 194 ++++++++++++--- crates/guara-core/src/fs/ext.rs | 248 ++++++++++++++----- crates/guara-core/src/fs/hfsplus.rs | 160 +++++++++--- crates/guara-core/src/fs/mod.rs | 2 +- crates/guara-core/src/fs/xfs.rs | 131 ++++++++-- crates/guara-core/src/image.rs | 26 +- crates/guara-core/src/jpegscan.rs | 98 ++++++-- crates/guara-core/src/lib.rs | 11 +- crates/guara-core/src/lvm.rs | 164 ++++++++++--- crates/guara-core/src/mp4rebuild.rs | 289 +++++++++++++++++----- crates/guara-core/src/partition.rs | 10 +- crates/guara-core/src/raid.rs | 111 +++++++-- crates/guara-core/src/raid_detect.rs | 87 +++++-- crates/guara-core/src/scan.rs | 48 +++- crates/guara-core/src/service.rs | 45 +++- crates/guara-core/src/signatures.rs | 50 ++-- crates/guara-core/src/source.rs | 6 +- crates/guara-core/src/types.rs | 47 +++- crates/guara-core/src/volume.rs | 13 +- crates/guara-core/tests/aff4.rs | 46 +++- crates/guara-core/tests/apfs.rs | 106 ++++++-- crates/guara-core/tests/bitlocker.rs | 34 ++- crates/guara-core/tests/btrfs.rs | 117 +++++++-- crates/guara-core/tests/defrag.rs | 60 ++++- crates/guara-core/tests/e2e_crypt.rs | 146 +++++++++-- crates/guara-core/tests/e2e_raid.rs | 137 +++++++++-- crates/guara-core/tests/ewf.rs | 56 ++++- crates/guara-core/tests/ext4.rs | 119 +++++++-- crates/guara-core/tests/filevault.rs | 8 +- crates/guara-core/tests/formats.rs | 86 ++++++- crates/guara-core/tests/hfsplus.rs | 25 +- crates/guara-core/tests/jpegscan.rs | 21 +- crates/guara-core/tests/luks.rs | 120 +++++++-- crates/guara-core/tests/lvm.rs | 3 +- crates/guara-core/tests/mp4rebuild.rs | 107 ++++++-- crates/guara-core/tests/raid.rs | 78 +++++- crates/guara-core/tests/raid_detect.rs | 56 ++++- crates/guara-core/tests/xfs.rs | 118 +++++++-- 49 files changed, 3806 insertions(+), 854 deletions(-) diff --git a/crates/guara-cli/src/main.rs b/crates/guara-cli/src/main.rs index 4b7747e..3c6c579 100644 --- a/crates/guara-cli/src/main.rs +++ b/crates/guara-cli/src/main.rs @@ -240,14 +240,23 @@ impl AsmArgs { for k in &self.volume_keys { secrets.push(SecretInput::Key { hex: k.clone() }); } - let raid = if self.raid || self.raid_level.is_some() || self.raid_chunk_kb.is_some() || self.raid_layout.is_some() || !self.raid_order.is_empty() { + let raid = if self.raid + || self.raid_level.is_some() + || self.raid_chunk_kb.is_some() + || self.raid_layout.is_some() + || !self.raid_order.is_empty() + { let level = match self.raid_level.as_deref() { None => None, Some("0") => Some(guara_core::raid::Level::R0), Some("1") => Some(guara_core::raid::Level::R1), Some("5") => Some(guara_core::raid::Level::R5), Some("6") => Some(guara_core::raid::Level::R6), - Some(x) => return Err(format!("nível de RAID não suportado: {x} (use 0, 1, 5 ou 6)")), + Some(x) => { + return Err(format!( + "nível de RAID não suportado: {x} (use 0, 1, 5 ou 6)" + )) + } }; let layout = match self.raid_layout.as_deref() { None => None, @@ -344,7 +353,12 @@ fn run(cli: Cli) -> Result<(), String> { } Cmd::Inspect { path, json, asm } => { let a = asm.assembly()?; - let v = eng.rpc("inspect", json!({ "path": path, "assembly": serde_json::to_value(&a).map_err(s)?})).map_err(s)?; + let v = eng + .rpc( + "inspect", + json!({ "path": path, "assembly": serde_json::to_value(&a).map_err(s)?}), + ) + .map_err(s)?; if json { println!("{}", serde_json::to_string_pretty(&v).unwrap()); } else { @@ -434,7 +448,14 @@ fn run(cli: Cli) -> Result<(), String> { } Cmd::Verify { path } => { let e = guara_core::ewf::EwfSource::open(std::path::Path::new(&path)).map_err(s)?; - println!("Formato: {}", if e.info.version == 2 { "Ex01 (EnCase 7)" } else { "E01 (EnCase)" }); + println!( + "Formato: {}", + if e.info.version == 2 { + "Ex01 (EnCase 7)" + } else { + "E01 (EnCase)" + } + ); println!("Segmentos: {}", e.info.segments); for (k, v) in &e.info.fields { println!("{k}: {v}"); @@ -452,7 +473,18 @@ fn run(cli: Cli) -> Result<(), String> { } } } - Cmd::Image { path, dest, format, case, examiner, description, evidence, notes, segment_mib, compression } => { + Cmd::Image { + path, + dest, + format, + case, + examiner, + description, + evidence, + notes, + segment_mib, + compression, + } => { let fmt = match format.as_str() { "raw" => guara_core::image::ImageFormat::Raw, "e01" => guara_core::image::ImageFormat::E01 { @@ -464,8 +496,17 @@ fn run(cli: Cli) -> Result<(), String> { segment_mib, compression, }, - "aff4" => guara_core::image::ImageFormat::Aff4 { case_number: case, examiner, evidence_number: evidence, notes }, - x => return Err(format!("formato de imagem desconhecido: {x} (use raw, e01 ou aff4)")), + "aff4" => guara_core::image::ImageFormat::Aff4 { + case_number: case, + examiner, + evidence_number: evidence, + notes, + }, + x => { + return Err(format!( + "formato de imagem desconhecido: {x} (use raw, e01 ou aff4)" + )) + } }; let job = eng.start_image_as(&path, &dest, &fmt).map_err(s)?; let st = wait(&eng, &job, None)?; @@ -1007,4 +1048,3 @@ fn print_health(v: &Value) { println!("\n{}", adv.as_str().unwrap_or("")); } } - diff --git a/crates/guara-core/src/aff4.rs b/crates/guara-core/src/aff4.rs index a0ef03c..d4efd9a 100644 --- a/crates/guara-core/src/aff4.rs +++ b/crates/guara-core/src/aff4.rs @@ -16,7 +16,10 @@ use std::sync::Mutex; pub fn is_aff4(head: &[u8]) -> bool { // ZIP local file header + primeiro nome "container.description" ou "version.txt". - head.len() > 50 && head.starts_with(b"PK\x03\x04") && (find(&head[30..head.len().min(80)], b"container.description") || find(&head[30..head.len().min(80)], b"version.txt")) + head.len() > 50 + && head.starts_with(b"PK\x03\x04") + && (find(&head[30..head.len().min(80)], b"container.description") + || find(&head[30..head.len().min(80)], b"version.txt")) } fn find(h: &[u8], n: &[u8]) -> bool { @@ -35,7 +38,10 @@ struct Resource { impl Resource { fn get(&self, k: &str) -> Option<&str> { - self.props.get(k).and_then(|v| v.first()).map(|s| s.as_str()) + self.props + .get(k) + .and_then(|v| v.first()) + .map(|s| s.as_str()) } fn num(&self, k: &str) -> Option { self.get(k)?.parse().ok() @@ -111,7 +117,10 @@ impl Turtle<'_> { } _ => { let s = self.i; - while self.i < self.b.len() && !self.b[self.i].is_ascii_whitespace() && !matches!(self.b[self.i], b';' | b',') { + while self.i < self.b.len() + && !self.b[self.i].is_ascii_whitespace() + && !matches!(self.b[self.i], b';' | b',') + { self.i += 1; } // ponto final de sentença colado ao termo @@ -128,7 +137,11 @@ impl Turtle<'_> { } fn parse_turtle(text: &str) -> HashMap { - let mut t = Turtle { b: text.as_bytes(), i: 0, prefixes: HashMap::new() }; + let mut t = Turtle { + b: text.as_bytes(), + i: 0, + prefixes: HashMap::new(), + }; let mut out: HashMap = HashMap::new(); loop { t.ws(); @@ -153,12 +166,14 @@ fn parse_turtle(text: &str) -> HashMap { continue; } // sujeito - let Some(subj) = (if t.b[t.i] == b':' && t.b.get(t.i + 1).map_or(true, |c| c.is_ascii_whitespace()) { - t.i += 1; - Some(t.prefixes.get("").cloned().unwrap_or_default()) - } else { - t.term() - }) else { + let Some(subj) = + (if t.b[t.i] == b':' && t.b.get(t.i + 1).map_or(true, |c| c.is_ascii_whitespace()) { + t.i += 1; + Some(t.prefixes.get("").cloned().unwrap_or_default()) + } else { + t.term() + }) + else { break; }; let res = out.entry(subj).or_default(); @@ -278,14 +293,23 @@ fn method_of(uri: &str) -> Method { impl Aff4Source { pub fn open(path: &Path) -> Result { let file = File::open(path)?; - let mut zip = zip::ZipArchive::new(file.try_clone()?).map_err(|e| Error::msg(format!("AFF4 inválido (ZIP): {e}")))?; + let mut zip = zip::ZipArchive::new(file.try_clone()?) + .map_err(|e| Error::msg(format!("AFF4 inválido (ZIP): {e}")))?; let mut entries = HashMap::new(); for i in 0..zip.len() { let f = zip.by_index_raw(i).map_err(|e| Error::msg(e.to_string()))?; - entries.insert(f.name().to_string(), Loc { data_start: f.data_start(), size: f.compressed_size() }); + entries.insert( + f.name().to_string(), + Loc { + data_start: f.data_start(), + size: f.compressed_size(), + }, + ); } let turtle = { - let mut f = zip.by_name("information.turtle").map_err(|_| Error::msg("AFF4 sem information.turtle"))?; + let mut f = zip + .by_name("information.turtle") + .map_err(|_| Error::msg("AFF4 sem information.turtle"))?; let mut s = String::new(); f.read_to_string(&mut s)?; s @@ -297,19 +321,30 @@ impl Aff4Source { let image = res .values() .find(|r| (r.is(&img_t) || r.is(&disk_t)) && r.get(&ds).is_some()) - .ok_or_else(|| Error::msg("AFF4 sem objeto de imagem (aff4:Image com aff4:dataStream)"))?; + .ok_or_else(|| { + Error::msg("AFF4 sem objeto de imagem (aff4:Image com aff4:dataStream)") + })?; let mut info = HashMap::new(); if let Some(v) = image.get(&format!("{NS}diskModel")) { info.insert("Modelo do disco".to_string(), v.to_string()); } - for (k, label) in [("caseNumber", "Número do caso"), ("examiner", "Examinador"), ("notes", "Notas"), ("evidenceNumber", "Número da evidência"), ("caseName", "Nome do caso")] { + for (k, label) in [ + ("caseNumber", "Número do caso"), + ("examiner", "Examinador"), + ("notes", "Notas"), + ("evidenceNumber", "Número da evidência"), + ("caseName", "Nome do caso"), + ] { if let Some(v) = res.values().find_map(|r| r.get(&format!("{NS}{k}"))) { info.insert(label.to_string(), v.to_string()); } } let data_urn = image.get(&ds).unwrap().to_string(); // Hashes gravados no fluxo de dados (ou no mapa). - for r in res.values().filter(|r| r.get(&format!("{NS}hash")).is_some()) { + for r in res + .values() + .filter(|r| r.get(&format!("{NS}hash")).is_some()) + { for h in r.props.get(&format!("{NS}hash")).into_iter().flatten() { match h.len() { 32 if h.chars().all(|c| c.is_ascii_hexdigit()) => { @@ -333,7 +368,9 @@ impl Aff4Source { cache: Mutex::new(None), index_cache: Mutex::new(HashMap::new()), }; - let top = res.get(&data_urn).ok_or_else(|| Error::msg("AFF4: fluxo de dados da imagem não encontrado"))?; + let top = res + .get(&data_urn) + .ok_or_else(|| Error::msg("AFF4: fluxo de dados da imagem não encontrado"))?; let map_t = format!("{NS}Map"); if top.is(&map_t) { me.size = top.num(&format!("{NS}size")).unwrap_or(0); @@ -345,7 +382,8 @@ impl Aff4Source { continue; } if let Some(sym) = line.strip_prefix(&format!("{NS}SymbolicStream")) { - let b = u8::from_str_radix(sym, 16).map_err(|_| Error::msg("AFF4: fluxo simbólico inválido"))?; + let b = u8::from_str_radix(sym, 16) + .map_err(|_| Error::msg("AFF4: fluxo simbólico inválido"))?; me.targets.push(Target::Fill(b)); } else if line == format!("{NS}Zero") { me.targets.push(Target::Zero); @@ -372,7 +410,12 @@ impl Aff4Source { me.size = s.size; me.streams.push(s); me.targets.push(Target::Stream(0)); - me.map = vec![MapEntry { start: 0, len: me.size, target_off: 0, target: 0 }]; + me.map = vec![MapEntry { + start: 0, + len: me.size, + target_off: 0, + target: 0, + }]; } if me.size == 0 { me.size = image.num(&format!("{NS}size")).unwrap_or(0); @@ -381,20 +424,29 @@ impl Aff4Source { } fn load_stream(&self, res: &HashMap, urn: &str) -> Result { - let r = res.get(urn).ok_or_else(|| Error::msg(format!("AFF4: fluxo {urn} não descrito")))?; + let r = res + .get(urn) + .ok_or_else(|| Error::msg(format!("AFF4: fluxo {urn} não descrito")))?; Ok(ImageStream { urn: urn.to_string(), chunk: r.num(&format!("{NS}chunkSize")).unwrap_or(32768), per_bevy: r.num(&format!("{NS}chunksInSegment")).unwrap_or(2048), - method: r.get(&format!("{NS}compressionMethod")).map(method_of).unwrap_or(Method::Stored), + method: r + .get(&format!("{NS}compressionMethod")) + .map(method_of) + .unwrap_or(Method::Stored), size: r.num(&format!("{NS}size")).unwrap_or(0), }) } /// MD5 do primeiro fluxo de blocos (o que o AFF4 grava em `aff4:hash`), para conferir a integridade. pub fn verify_stream_md5(&self) -> Result> { - let Some(want) = self.info.get("md5").cloned() else { return Ok(None) }; - let Some(st) = self.streams.first() else { return Ok(None) }; + let Some(want) = self.info.get("md5").cloned() else { + return Ok(None); + }; + let Some(st) = self.streams.first() else { + return Ok(None); + }; let mut h = MultiHasher::new(); let mut buf = vec![0u8; st.chunk as usize]; let mut pos = 0u64; @@ -408,7 +460,10 @@ impl Aff4Source { } fn read_entry(&self, name: &str) -> Result> { - let loc = self.entries.get(name).ok_or_else(|| Error::msg(format!("AFF4: entrada {name} ausente")))?; + let loc = self + .entries + .get(name) + .ok_or_else(|| Error::msg(format!("AFF4: entrada {name} ausente")))?; let mut v = vec![0u8; loc.size as usize]; read_exact_at_file(&self.file, loc.data_start, &mut v)?; Ok(v) @@ -431,7 +486,9 @@ impl Aff4Source { if let Some(i) = ic.get(&(si, bevy)) { i.clone() } else { - let loc = self.entries.get(&format!("{base}.index")).ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "índice do bevy ausente"))?; + let loc = self.entries.get(&format!("{base}.index")).ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "índice do bevy ausente") + })?; let mut v = vec![0u8; loc.size as usize]; read_exact_at_file(&self.file, loc.data_start, &mut v)?; let a = std::sync::Arc::new(v); @@ -439,9 +496,17 @@ impl Aff4Source { a } }; - let e = index.get(within as usize * 12..within as usize * 12 + 12).ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "bloco fora do índice"))?; - let (off, len) = (u64::from_le_bytes(e[0..8].try_into().unwrap()), u32::from_le_bytes(e[8..12].try_into().unwrap()) as usize); - let loc = self.entries.get(&base).ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "bevy ausente"))?; + let e = index + .get(within as usize * 12..within as usize * 12 + 12) + .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "bloco fora do índice"))?; + let (off, len) = ( + u64::from_le_bytes(e[0..8].try_into().unwrap()), + u32::from_le_bytes(e[8..12].try_into().unwrap()) as usize, + ); + let loc = self + .entries + .get(&base) + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "bevy ausente"))?; let mut raw = vec![0u8; len]; read_exact_at_file(&self.file, loc.data_start + off, &mut raw)?; let want = st.chunk.min(st.size.saturating_sub(chunk_no * st.chunk)) as usize; @@ -450,16 +515,23 @@ impl Aff4Source { } else { let bad = |m: String| io::Error::new(io::ErrorKind::InvalidData, m); match st.method { - Method::Snappy => snap::raw::Decoder::new().decompress_vec(&raw).map_err(|e| bad(format!("snappy: {e}")))?, - Method::Lz4 => lz4_flex::block::decompress(&raw, st.chunk as usize).map_err(|e| bad(format!("lz4: {e}")))?, + Method::Snappy => snap::raw::Decoder::new() + .decompress_vec(&raw) + .map_err(|e| bad(format!("snappy: {e}")))?, + Method::Lz4 => lz4_flex::block::decompress(&raw, st.chunk as usize) + .map_err(|e| bad(format!("lz4: {e}")))?, Method::Deflate => { let mut v = Vec::with_capacity(st.chunk as usize); - flate2::read::DeflateDecoder::new(&raw[..]).read_to_end(&mut v).map_err(|e| bad(format!("deflate: {e}")))?; + flate2::read::DeflateDecoder::new(&raw[..]) + .read_to_end(&mut v) + .map_err(|e| bad(format!("deflate: {e}")))?; v } Method::Zlib => { let mut v = Vec::with_capacity(st.chunk as usize); - flate2::read::ZlibDecoder::new(&raw[..]).read_to_end(&mut v).map_err(|e| bad(format!("zlib: {e}")))?; + flate2::read::ZlibDecoder::new(&raw[..]) + .read_to_end(&mut v) + .map_err(|e| bad(format!("zlib: {e}")))?; v } Method::Stored => unreachable!(), @@ -499,9 +571,13 @@ fn read_exact_at_file(f: &File, off: u64, buf: &mut [u8]) -> io::Result<()> { { let mut done = 0; while done < buf.len() { - let n = std::os::windows::fs::FileExt::seek_read(f, &mut buf[done..], off + done as u64)?; + let n = + std::os::windows::fs::FileExt::seek_read(f, &mut buf[done..], off + done as u64)?; if n == 0 { - return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "AFF4 truncado")); + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "AFF4 truncado", + )); } done += n; } @@ -534,7 +610,9 @@ impl Source for Aff4Source { let n = ((e.start + e.len - pos) as usize).min(want - done); let toff = e.target_off + (pos - e.start); match self.targets.get(e.target as usize) { - Some(Target::Stream(si)) => self.read_stream(*si, toff, &mut buf[done..done + n])?, + Some(Target::Stream(si)) => { + self.read_stream(*si, toff, &mut buf[done..done + n])? + } Some(Target::Fill(b)) => buf[done..done + n].fill(*b), _ => buf[done..done + n].fill(0), } @@ -557,7 +635,13 @@ pub struct Aff4Options { impl Default for Aff4Options { fn default() -> Self { - Self { case_name: String::new(), examiner: String::new(), notes: String::new(), evidence_number: String::new(), chunk_size: 32768 } + Self { + case_name: String::new(), + examiner: String::new(), + notes: String::new(), + evidence_number: String::new(), + chunk_size: 32768, + } } } @@ -565,17 +649,30 @@ fn new_uuid() -> String { let mut b = [0u8; 16]; let _ = std::fs::File::open("/dev/urandom").and_then(|mut f| f.read_exact(&mut b)); if b == [0u8; 16] { - let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(1); + let t = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(1); b = (t ^ (std::process::id() as u128) << 64).to_le_bytes(); } b[6] = (b[6] & 0x0F) | 0x40; b[8] = (b[8] & 0x3F) | 0x80; let h: Vec = b.iter().map(|x| format!("{x:02x}")).collect(); - format!("{}-{}-{}-{}-{}", h[0..4].concat(), h[4..6].concat(), h[6..8].concat(), h[8..10].concat(), h[10..16].concat()) + format!( + "{}-{}-{}-{}-{}", + h[0..4].concat(), + h[4..6].concat(), + h[6..8].concat(), + h[8..10].concat(), + h[10..16].concat() + ) } fn iso_now() -> String { - let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0); + let secs = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); let days = secs.div_euclid(86400); let rem = secs.rem_euclid(86400); let z = days + 719468; @@ -588,11 +685,18 @@ fn iso_now() -> String { let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = if m <= 2 { y + 1 } else { y }; - format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}.000Z", rem / 3600, rem % 3600 / 60, rem % 60) + format!( + "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}.000Z", + rem / 3600, + rem % 3600 / 60, + rem % 60 + ) } fn esc(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n") + s.replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\n', "\\n") } /// Grava uma imagem AFF4 linear (um fluxo de blocos snappy) de `src`. @@ -607,7 +711,12 @@ pub fn write_aff4( .write(true) .create_new(true) .open(dest) - .map_err(|e| Error::msg(format!("não foi possível criar {}: {e} (o arquivo não pode existir)", dest.display())))?; + .map_err(|e| { + Error::msg(format!( + "não foi possível criar {}: {e} (o arquivo não pode existir)", + dest.display() + )) + })?; crate::owner::give_to_user(dest); let total = src.size(); let chunk = opts.chunk_size as u64; @@ -616,9 +725,12 @@ pub fn write_aff4( let stream = format!("aff4://{}", new_uuid()); let image = format!("aff4://{}", new_uuid()); let mut zip = zip::ZipWriter::new(out); - let stored = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored).large_file(true); + let stored = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored) + .large_file(true); let result = (|| -> Result { - zip.start_file("container.description", stored).map_err(zerr)?; + zip.start_file("container.description", stored) + .map_err(zerr)?; zip.write_all(volume.as_bytes())?; zip.start_file("version.txt", stored).map_err(zerr)?; zip.write_all(format!("major=1\nminor=2\ntool=Guara {}\n", crate::VERSION).as_bytes())?; @@ -661,9 +773,15 @@ pub fn write_aff4( } hasher.update(b); // bloco curto (último) é guardado como está; comprimido só se ficar menor - let comp = enc_snap.compress_vec(b).map_err(|e| Error::msg(e.to_string()))?; + let comp = enc_snap + .compress_vec(b) + .map_err(|e| Error::msg(e.to_string()))?; // Só um bloco cheio pode ficar sem compressão (comprimento == tamanho do bloco). - let data: &[u8] = if want as u64 == chunk && comp.len() >= want { b } else { &comp }; + let data: &[u8] = if want as u64 == chunk && comp.len() >= want { + b + } else { + &comp + }; zip.write_all(data)?; index.extend_from_slice(&boff.to_le_bytes()); index.extend_from_slice(&(data.len() as u32).to_le_bytes()); @@ -672,7 +790,8 @@ pub fn write_aff4( chunk_no += 1; progress(pos, total); } - zip.start_file(format!("{name}.index").as_str(), stored).map_err(zerr)?; + zip.start_file(format!("{name}.index").as_str(), stored) + .map_err(zerr)?; zip.write_all(&index)?; if rep.cancelled { break; @@ -708,7 +827,10 @@ pub fn write_aff4( match result { Ok(rep) => { zip.finish().map_err(zerr)?; - crate::owner::write_owned(&dest.with_extension("map.json"), serde_json::to_vec_pretty(&rep)?)?; + crate::owner::write_owned( + &dest.with_extension("map.json"), + serde_json::to_vec_pretty(&rep)?, + )?; Ok(rep) } Err(e) => { diff --git a/crates/guara-core/src/assemble.rs b/crates/guara-core/src/assemble.rs index 292f1c2..710552c 100644 --- a/crates/guara-core/src/assemble.rs +++ b/crates/guara-core/src/assemble.rs @@ -15,12 +15,20 @@ use std::sync::Arc; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "lowercase")] pub enum SecretInput { - Password { value: String }, - Recovery { value: String }, + Password { + value: String, + }, + Recovery { + value: String, + }, /// Caminho de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS). - Keyfile { path: String }, + Keyfile { + path: String, + }, /// Chave do volume em hexadecimal. - Key { hex: String }, + Key { + hex: String, + }, } /// Chave de volume guardada na sessão (só quando o usuário pede para lembrar). @@ -62,7 +70,10 @@ pub struct Assembly { impl Assembly { pub fn is_plain(&self) -> bool { - self.members.is_empty() && self.secrets.is_empty() && self.keys.is_empty() && self.raid.is_none() + self.members.is_empty() + && self.secrets.is_empty() + && self.keys.is_empty() + && self.raid.is_none() } } @@ -102,11 +113,14 @@ fn to_secrets(a: &Assembly) -> Result> { v.push(match s { SecretInput::Password { value } => Secret::Password(value.clone()), SecretInput::Recovery { value } => Secret::RecoveryKey(value.clone()), - SecretInput::Keyfile { path } => Secret::KeyFile( - std::fs::read(path).map_err(|e| Error::msg(format!("não foi possível ler o arquivo de chave {path}: {e}")))?, - ), + SecretInput::Keyfile { path } => Secret::KeyFile(std::fs::read(path).map_err(|e| { + Error::msg(format!( + "não foi possível ler o arquivo de chave {path}: {e}" + )) + })?), SecretInput::Key { hex } => Secret::VolumeKey( - crypt::from_hex(hex).ok_or_else(|| Error::msg("chave do volume em hexadecimal inválida"))?, + crypt::from_hex(hex) + .ok_or_else(|| Error::msg("chave do volume em hexadecimal inválida"))?, ), }); } @@ -142,8 +156,16 @@ const MIB: u64 = 1 << 20; /// Coloca `src` em `start`; se cair dentro de uma região virtual, a substitui por dentro. fn patch(regions: &mut Vec, start: u64, src: Arc, label: String) { let len = src.size(); - if let Some(r) = regions.iter_mut().find(|r| r.start <= start && start < r.start + r.len) { - let inner = Region { start: start - r.start, len, src, label: label.clone() }; + if let Some(r) = regions + .iter_mut() + .find(|r| r.start <= start && start < r.start + r.len) + { + let inner = Region { + start: start - r.start, + len, + src, + label: label.clone(), + }; let wrapped = Overlay::new(r.src.clone(), vec![inner]); if start == r.start { r.len = r.len.max(len); @@ -151,7 +173,12 @@ fn patch(regions: &mut Vec, start: u64, src: Arc, label: Str r.src = Arc::new(wrapped); return; } - regions.push(Region { start, len, src, label }); + regions.push(Region { + start, + len, + src, + label, + }); } /// Candidato a membro de RAID/LVM: uma janela de disco (inteiro ou partição). @@ -162,10 +189,17 @@ struct Window { } fn windows_of(src: &Arc, primary: bool) -> Vec { - let mut v = vec![Window { src: src.clone(), primary_start: primary.then_some(0) }]; + let mut v = vec![Window { + src: src.clone(), + primary_start: primary.then_some(0), + }]; let layout = partition::analyze(src.as_ref(), false); for p in &layout.partitions { - let len = if p.len == 0 { src.size().saturating_sub(p.start) } else { p.len }; + let len = if p.len == 0 { + src.size().saturating_sub(p.start) + } else { + p.len + }; v.push(Window { src: Arc::new(SubSource::new(src.clone(), p.start, len)), primary_start: primary.then_some(p.start), @@ -196,11 +230,21 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result> = vec![base.clone()]; disks.extend(members.iter().cloned()); let params = pick_raid(&disks, spec, &mut out.notes)?; - let src: Arc = Arc::new(crate::raid::RaidSource::new(params).map_err(Error::msg)?); - out.notes.push(format!("RAID montado a partir de {} disco(s) sem metadados.", disks.len())); + let src: Arc = + Arc::new(crate::raid::RaidSource::new(params).map_err(Error::msg)?); + out.notes.push(format!( + "RAID montado a partir de {} disco(s) sem metadados.", + disks.len() + )); out.skip_primary = true; members.clear(); - add_virtual(&mut regions, &mut next_virtual, &mut out, src, "RAID".into()); + add_virtual( + &mut regions, + &mut next_virtual, + &mut out, + src, + "RAID".into(), + ); } let mut done_containers: std::collections::BTreeSet = Default::default(); let mut tried_pool = false; @@ -219,11 +263,21 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result = vec![]; - pool.push(Window { src: current.clone(), primary_start: Some(0) }); + pool.push(Window { + src: current.clone(), + primary_start: Some(0), + }); for p in &all_parts { if matches!(p.fs, FsKind::Raid | FsKind::Lvm) { - let len = if p.len == 0 { current.size().saturating_sub(p.start) } else { p.len }; - pool.push(Window { src: Arc::new(SubSource::new(current.clone(), p.start, len)), primary_start: Some(p.start) }); + let len = if p.len == 0 { + current.size().saturating_sub(p.start) + } else { + p.len + }; + pool.push(Window { + src: Arc::new(SubSource::new(current.clone(), p.start, len)), + primary_start: Some(p.start), + }); } } for m in &members { @@ -241,7 +295,12 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result = md.into_iter().filter(|(_, s)| !done_containers.contains(&format!("md{}", crypt::to_hex(&s.uuid)))).collect(); + let new_md: Vec<_> = md + .into_iter() + .filter(|(_, s)| { + !done_containers.contains(&format!("md{}", crypt::to_hex(&s.uuid))) + }) + .collect(); if !new_md.is_empty() { for (_, s) in &new_md { done_containers.insert(format!("md{}", crypt::to_hex(&s.uuid))); @@ -249,13 +308,30 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result { - let label = format!("{} {}", a.info.level.name(), if a.name.is_empty() { a.uuid.clone() } else { a.name.clone() }); + let label = format!( + "{} {}", + a.info.level.name(), + if a.name.is_empty() { + a.uuid.clone() + } else { + a.name.clone() + } + ); if a.missing > 0 { - out.notes.push(format!("{label}: montado sem {} membro(s) (modo degradado).", a.missing)); + out.notes.push(format!( + "{label}: montado sem {} membro(s) (modo degradado).", + a.missing + )); } else { out.notes.push(format!("{label} montado.")); } - add_virtual(&mut regions, &mut next_virtual, &mut out, a.source.clone(), label); + add_virtual( + &mut regions, + &mut next_virtual, + &mut out, + a.source.clone(), + label, + ); progress = true; } Err(e) => out.notes.push(format!("RAID não montado: {e}")), @@ -267,7 +343,10 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result, crate::lvm::PvInfo)> = vec![]; let mut pv_primary: Vec = vec![]; let md_srcs: Vec> = regions.iter().map(|r| r.src.clone()).collect(); - let mut lvm_pool: Vec<(Arc, Option)> = pool.iter().map(|w| (w.src.clone(), w.primary_start)).collect(); + let mut lvm_pool: Vec<(Arc, Option)> = pool + .iter() + .map(|w| (w.src.clone(), w.primary_start)) + .collect(); for (r, s) in regions.iter().zip(md_srcs) { lvm_pool.push((s, Some(r.start))); } @@ -288,7 +367,13 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result, asm: &Assembly) -> Result = Arc::new(SubSource::new(current.clone(), p.start, len)); let mut all = secrets.clone(); for k in asm.keys.iter().filter(|k| k.start == p.start) { @@ -366,7 +455,13 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result, next: &mut u64, out: &mut Assembled, src: Arc, label: String) { +fn add_virtual( + regions: &mut Vec, + next: &mut u64, + out: &mut Assembled, + src: Arc, + label: String, +) { let start = *next; let len = src.size(); if len == 0 { @@ -393,15 +488,34 @@ fn add_virtual(regions: &mut Vec, next: &mut u64, out: &mut Assembled, s out.extra_parts.push(q); } } - regions.push(Region { start, len, src, label }); + regions.push(Region { + start, + len, + src, + label, + }); } /// Escolhe os parâmetros do RAID: o que o usuário deu manda; o resto vem da detecção. -fn pick_raid(disks: &[Arc], spec: &RaidSpec, notes: &mut Vec) -> Result { +fn pick_raid( + disks: &[Arc], + spec: &RaidSpec, + notes: &mut Vec, +) -> Result { use crate::raid::{Member, RaidParams}; - if let (Some(level), Some(chunk), Some(layout), Some(order)) = (spec.level, spec.chunk_kb, spec.layout, spec.order.clone()) { + if let (Some(level), Some(chunk), Some(layout), Some(order)) = + (spec.level, spec.chunk_kb, spec.layout, spec.order.clone()) + { if order.len() == disks.len() && order.iter().all(|&i| i < disks.len()) { - let members = order.iter().map(|&i| Some(Member { src: disks[i].clone(), data_offset: 0 })).collect(); + let members = order + .iter() + .map(|&i| { + Some(Member { + src: disks[i].clone(), + data_offset: 0, + }) + }) + .collect(); return Ok(RaidParams::simple(level, chunk * 1024, layout, members)); } } diff --git a/crates/guara-core/src/crypt/bitlocker.rs b/crates/guara-core/src/crypt/bitlocker.rs index b9beb0a..c44fed6 100644 --- a/crates/guara-core/src/crypt/bitlocker.rs +++ b/crates/guara-core/src/crypt/bitlocker.rs @@ -16,8 +16,12 @@ fn le64(b: &[u8], o: usize) -> u64 { u64::from_le_bytes(b[o..o + 8].try_into().unwrap()) } -const GUID_NORMAL: [u8; 16] = [0x3b, 0xd6, 0x67, 0x49, 0x29, 0x2e, 0xd8, 0x4a, 0x83, 0x99, 0xf6, 0xa3, 0x39, 0xe3, 0xd0, 0x01]; -const GUID_EOW: [u8; 16] = [0x3b, 0x4d, 0xa8, 0x92, 0x80, 0xdd, 0x0e, 0x4d, 0x9e, 0x4e, 0xb1, 0xe3, 0x28, 0x4e, 0xae, 0xd8]; +const GUID_NORMAL: [u8; 16] = [ + 0x3b, 0xd6, 0x67, 0x49, 0x29, 0x2e, 0xd8, 0x4a, 0x83, 0x99, 0xf6, 0xa3, 0x39, 0xe3, 0xd0, 0x01, +]; +const GUID_EOW: [u8; 16] = [ + 0x3b, 0x4d, 0xa8, 0x92, 0x80, 0xdd, 0x0e, 0x4d, 0x9e, 0x4e, 0xb1, 0xe3, 0x28, 0x4e, 0xae, 0xd8, +]; /// Posição da tabela de deslocamentos FVE, se for BitLocker (normal ou To Go). fn offsets_pos(vbr: &[u8]) -> Option { @@ -45,7 +49,9 @@ const RA: [u32; 4] = [9, 0, 13, 0]; const RB: [u32; 4] = [0, 10, 0, 25]; fn words(buf: &[u8]) -> Vec { - buf.chunks_exact(4).map(|c| u32::from_le_bytes(c.try_into().unwrap())).collect() + buf.chunks_exact(4) + .map(|c| u32::from_le_bytes(c.try_into().unwrap())) + .collect() } // Decifragem (ordem do dm-crypt do Linux): somas em ordem crescente. @@ -219,7 +225,11 @@ impl Vmk { self.nested.iter().find(|e| e.vtype == 5) } fn stretch_salt(&self) -> Option<&[u8]> { - self.nested.iter().find(|e| e.vtype == 3).filter(|e| e.data.len() >= 20).map(|e| &e.data[4..20]) + self.nested + .iter() + .find(|e| e.vtype == 3) + .filter(|e| e.data.len() >= 20) + .map(|e| &e.data[4..20]) } } @@ -366,8 +376,13 @@ pub fn unlock(src: Arc, secrets: &[Secret]) -> UnlockResult { } } } - let fve = fve.ok_or_else(|| UnlockError::Unsupported("metadados FVE ilegíveis ou do Windows Vista".into()))?; - let fvek_entry = fve.fvek.as_ref().ok_or_else(|| UnlockError::Corrupt("sem FVEK".into()))?; + let fve = fve.ok_or_else(|| { + UnlockError::Unsupported("metadados FVE ilegíveis ou do Windows Vista".into()) + })?; + let fvek_entry = fve + .fvek + .as_ref() + .ok_or_else(|| UnlockError::Corrupt("sem FVEK".into()))?; let mut volume_key: Option> = None; for s in secrets { @@ -393,7 +408,8 @@ pub fn unlock(src: Arc, secrets: &[Secret]) -> UnlockResult { k } else { let vmk = vmk_found.ok_or(UnlockError::WrongSecret)?; - decrypt_key_entry(&vmk, fvek_entry).ok_or_else(|| UnlockError::Corrupt("FVEK não decifra com a VMK".into()))? + decrypt_key_entry(&vmk, fvek_entry) + .ok_or_else(|| UnlockError::Corrupt("FVEK não decifra com a VMK".into()))? }; let k = &fvek_bytes; let bad = || UnlockError::Corrupt("tamanho de chave incompatível com o método".into()); @@ -408,7 +424,11 @@ pub fn unlock(src: Arc, secrets: &[Secret]) -> UnlockResult { key: AesAny::new(&k[..n]).ok_or_else(bad)?, tweak: AesAny::new(&k[32..32 + n]).ok_or_else(bad)?, }, - if n == 16 { "AES-128-CBC + difusor" } else { "AES-256-CBC + difusor" }, + if n == 16 { + "AES-128-CBC + difusor" + } else { + "AES-256-CBC + difusor" + }, ) } 0x8002 | 0x8003 => { @@ -417,35 +437,69 @@ pub fn unlock(src: Arc, secrets: &[Secret]) -> UnlockResult { return Err(bad()); } let key = AesAny::new(&k[..n]).ok_or_else(bad)?; - (SectorCipher::Cbc { key: key.clone(), iv: Iv::Eboiv(key) }, if n == 16 { "AES-128-CBC" } else { "AES-256-CBC" }) + ( + SectorCipher::Cbc { + key: key.clone(), + iv: Iv::Eboiv(key), + }, + if n == 16 { + "AES-128-CBC" + } else { + "AES-256-CBC" + }, + ) } 0x8004 | 0x8005 => { let n = if fve.method == 0x8004 { 16 } else { 32 }; if k.len() < 2 * n { return Err(bad()); } - let iv = if bps > 512 { Iv::Unit(bps) } else { Iv::Plain64 }; + let iv = if bps > 512 { + Iv::Unit(bps) + } else { + Iv::Plain64 + }; ( SectorCipher::Xts { k1: AesAny::new(&k[..n]).ok_or_else(bad)?, k2: AesAny::new(&k[n..2 * n]).ok_or_else(bad)?, iv, }, - if n == 16 { "AES-128-XTS" } else { "AES-256-XTS" }, + if n == 16 { + "AES-128-XTS" + } else { + "AES-256-XTS" + }, ) } - m => return Err(UnlockError::Unsupported(format!("método de cifragem BitLocker {m:#x}"))), + m => { + return Err(UnlockError::Unsupported(format!( + "método de cifragem BitLocker {m:#x}" + ))) + } }; let len = src.size(); let mut cs = CryptSource::new(src.clone(), cipher, 0, len, bps); let mut patches = vec![]; // Áreas de metadados FVE: o Windows não as expõe como dados. for off in meta_offsets { - patches.push(Patch { at: off, len: fve.meta_size, from: 0, decrypt_from: None, zero: true }); + patches.push(Patch { + at: off, + len: fve.meta_size, + from: 0, + decrypt_from: None, + zero: true, + }); } if fve.volume_header_size > 0 && fve.volume_header_offset > 0 { // O início do volume original foi movido para outro lugar e cifrado lá. - patches.push(Patch { at: fve.volume_header_offset, len: fve.volume_header_size, from: 0, decrypt_from: None, zero: true }); + patches.push(Patch { + at: fve.volume_header_offset, + len: fve.volume_header_size, + from: 0, + decrypt_from: None, + zero: true, + }); patches.push(Patch { at: 0, len: fve.volume_header_size, @@ -465,6 +519,11 @@ pub fn unlock(src: Arc, secrets: &[Secret]) -> UnlockResult { }) } -pub fn unlock_window(base: Arc, start: u64, len: u64, secrets: &[Secret]) -> UnlockResult { +pub fn unlock_window( + base: Arc, + start: u64, + len: u64, + secrets: &[Secret], +) -> UnlockResult { unlock(Arc::new(SubSource::new(base, start, len)), secrets) } diff --git a/crates/guara-core/src/crypt/filevault.rs b/crates/guara-core/src/crypt/filevault.rs index e0f5792..fcab4fe 100644 --- a/crates/guara-core/src/crypt/filevault.rs +++ b/crates/guara-core/src/crypt/filevault.rs @@ -80,12 +80,18 @@ fn plist_value(xml: &str, key: &str) -> Option { while let Some(p) = xml[from..].find(&k) { let rest = &xml[from + p + k.len()..]; from += p + k.len(); - let Some(open) = rest.find('<').filter(|&o| o == 0) else { continue }; - let Some(gt) = rest[open..].find('>') else { continue }; + let Some(open) = rest.find('<').filter(|&o| o == 0) else { + continue; + }; + let Some(gt) = rest[open..].find('>') else { + continue; + }; if rest[open..open + gt].ends_with('/') { continue; // } - let Some(te) = rest[open + 1..].find(|c: char| c == ' ' || c == '>') else { continue }; + let Some(te) = rest[open + 1..].find([' ', '>']) else { + continue; + }; let tag = &rest[open + 1..open + 1 + te]; let vstart = open + gt + 1; if let Some(e) = rest[vstart..].find(&format!("")) { @@ -116,20 +122,31 @@ pub fn unlock(src: Arc, secrets: &[Secret]) -> UnlockResult { let block_size = le32(&h, 96) as u64; let disklbl = le64(&h, 104); if block_size == 0 || block_size > 1 << 20 || le32(&h, 168) != 16 { - return Err(UnlockError::Unsupported("Core Storage com chave de metadados que não é AES-128".into())); + return Err(UnlockError::Unsupported( + "Core Storage com chave de metadados que não é AES-128".into(), + )); } let mut md_key = [0u8; 32]; md_key[..16].copy_from_slice(&h[176..192]); md_key[16..].copy_from_slice(&h[304..320]); - let (k1, k2) = (AesAny::new(&md_key[..16]).unwrap(), AesAny::new(&md_key[16..]).unwrap()); - let md_cipher = SectorCipher::Xts { k1, k2, iv: Iv::Plain64 }; + let (k1, k2) = ( + AesAny::new(&md_key[..16]).unwrap(), + AesAny::new(&md_key[16..]).unwrap(), + ); + let md_cipher = SectorCipher::Xts { + k1, + k2, + iv: Iv::Plain64, + }; - let label = read_vec(src.as_ref(), disklbl * block_size, MD_BLOCK).map_err(|e| corrupt(&e.to_string()))?; + let label = read_vec(src.as_ref(), disklbl * block_size, MD_BLOCK) + .map_err(|e| corrupt(&e.to_string()))?; if label.len() < MD_BLOCK { return Err(corrupt("rótulo do disco truncado")); } let vgd_off = le32(&label, 220) as u64; - let vgd = read_vec(src.as_ref(), disklbl * block_size + vgd_off, 40).map_err(|e| corrupt(&e.to_string()))?; + let vgd = read_vec(src.as_ref(), disklbl * block_size + vgd_off, 40) + .map_err(|e| corrupt(&e.to_string()))?; if vgd.len() < 40 { return Err(corrupt("descritor de grupos truncado")); } @@ -141,7 +158,8 @@ pub fn unlock(src: Arc, secrets: &[Secret]) -> UnlockResult { let mut family = [0u8; 16]; let mut got = 0u8; for i in 0..md_blocks.min(64) { - let mut b = read_vec(src.as_ref(), md_start + i * MD_BLOCK as u64, MD_BLOCK).map_err(|e| corrupt(&e.to_string()))?; + let mut b = read_vec(src.as_ref(), md_start + i * MD_BLOCK as u64, MD_BLOCK) + .map_err(|e| corrupt(&e.to_string()))?; if b.len() < MD_BLOCK || b.iter().all(|&x| x == 0) { break; } @@ -192,7 +210,12 @@ pub fn unlock(src: Arc, secrets: &[Secret]) -> UnlockResult { let finish = |vk16: [u8; 16]| -> UnlockResult { let mut full = [0u8; 32]; full[..16].copy_from_slice(&vk16); - full[16..].copy_from_slice(&Sha256::new().chain_update(vk16).chain_update(family).finalize()[..16]); + full[16..].copy_from_slice( + &Sha256::new() + .chain_update(vk16) + .chain_update(family) + .finalize()[..16], + ); let cipher = SectorCipher::Xts { k1: AesAny::new(&full[..16]).unwrap(), k2: AesAny::new(&full[16..]).unwrap(), @@ -228,6 +251,11 @@ pub fn unlock(src: Arc, secrets: &[Secret]) -> UnlockResult { Err(UnlockError::WrongSecret) } -pub fn unlock_window(base: Arc, start: u64, len: u64, secrets: &[Secret]) -> UnlockResult { +pub fn unlock_window( + base: Arc, + start: u64, + len: u64, + secrets: &[Secret], +) -> UnlockResult { unlock(Arc::new(SubSource::new(base, start, len)), secrets) } diff --git a/crates/guara-core/src/crypt/luks.rs b/crates/guara-core/src/crypt/luks.rs index 7c0753a..465fd08 100644 --- a/crates/guara-core/src/crypt/luks.rs +++ b/crates/guara-core/src/crypt/luks.rs @@ -89,9 +89,13 @@ pub fn af_merge(src: &[u8], block: usize, stripes: usize, h: Hash) -> Vec { /// Cifra descrita por texto como "aes-xts-plain64" ou "aes-cbc-essiv:sha256". pub fn parse_cipher(spec: &str, key: &[u8]) -> std::result::Result { - let (alg, rest) = spec.split_once('-').ok_or_else(|| UnlockError::Unsupported(spec.into()))?; + let (alg, rest) = spec + .split_once('-') + .ok_or_else(|| UnlockError::Unsupported(spec.into()))?; if !alg.eq_ignore_ascii_case("aes") { - return Err(UnlockError::Unsupported(format!("cifra {alg} (apenas AES)"))); + return Err(UnlockError::Unsupported(format!( + "cifra {alg} (apenas AES)" + ))); } let (mode, ivs) = rest.split_once('-').unwrap_or((rest, "plain")); let bad = || UnlockError::Unsupported(format!("chave inválida para {spec}")); @@ -102,7 +106,8 @@ pub fn parse_cipher(spec: &str, key: &[u8]) -> std::result::Result Iv::Plain32, "plain64be" => Iv::Plain64Be, s if s.starts_with("essiv:") => { - let h = Hash::parse(&s[6..]).ok_or_else(|| UnlockError::Unsupported(spec.into()))?; + let h = + Hash::parse(&s[6..]).ok_or_else(|| UnlockError::Unsupported(spec.into()))?; Iv::Essiv(AesAny::new(&h.digest(&[k])).ok_or_else(bad)?) } _ => return Err(UnlockError::Unsupported(format!("IV {ivs}"))), @@ -181,7 +186,8 @@ fn unlock_v1(src: Arc, secrets: &[Secret]) -> UnlockResult { } let cipher_name = cstr(&h[8..40]); let cipher_mode = cstr(&h[40..72]); - let hash = Hash::parse(&cstr(&h[72..104])).ok_or_else(|| UnlockError::Unsupported(format!("hash {}", cstr(&h[72..104]))))?; + let hash = Hash::parse(&cstr(&h[72..104])) + .ok_or_else(|| UnlockError::Unsupported(format!("hash {}", cstr(&h[72..104]))))?; let payload = be32(&h, 104) as u64 * 512; let key_bytes = be32(&h, 108) as usize; let mk_digest = &h[112..132]; @@ -221,7 +227,9 @@ fn unlock_v1(src: Arc, secrets: &[Secret]) -> UnlockResult { continue; } let km_len = (key_bytes * stripes).div_ceil(512) * 512; - let Ok(mut km) = read_vec(src.as_ref(), km_off, km_len) else { continue }; + let Ok(mut km) = read_vec(src.as_ref(), km_off, km_len) else { + continue; + }; if km.len() < km_len { continue; } @@ -284,25 +292,38 @@ fn unlock_v2(src: Arc, secrets: &[Secret]) -> UnlockResult { } let hdr_size = u64::from_be_bytes(h[8..16].try_into().unwrap()); if !(16384..=(4 << 20)).contains(&hdr_size) { - return Err(UnlockError::Corrupt("tamanho de cabeçalho LUKS2 inválido".into())); + return Err(UnlockError::Corrupt( + "tamanho de cabeçalho LUKS2 inválido".into(), + )); } - let area = read_vec(src.as_ref(), 4096, (hdr_size - 4096) as usize).map_err(|e| UnlockError::Corrupt(e.to_string()))?; + let area = read_vec(src.as_ref(), 4096, (hdr_size - 4096) as usize) + .map_err(|e| UnlockError::Corrupt(e.to_string()))?; let end = area.iter().position(|&c| c == 0).unwrap_or(area.len()); - let json: serde_json::Value = serde_json::from_slice(&area[..end]).map_err(|e| UnlockError::Corrupt(format!("JSON: {e}")))?; - let segments = json.get("segments").and_then(|s| s.as_object()).ok_or_else(|| UnlockError::Corrupt("sem segments".into()))?; + let json: serde_json::Value = serde_json::from_slice(&area[..end]) + .map_err(|e| UnlockError::Corrupt(format!("JSON: {e}")))?; + let segments = json + .get("segments") + .and_then(|s| s.as_object()) + .ok_or_else(|| UnlockError::Corrupt("sem segments".into()))?; let (_, seg) = segments .iter() .find(|(_, s)| jstr(s, "type") == Some("crypt")) .ok_or_else(|| UnlockError::Unsupported("sem segmento cifrado".into()))?; - let seg_off = jnum(seg, "offset").ok_or_else(|| UnlockError::Corrupt("offset do segmento".into()))?; + let seg_off = + jnum(seg, "offset").ok_or_else(|| UnlockError::Corrupt("offset do segmento".into()))?; let seg_len = match seg.get("size").and_then(|s| s.as_str()) { Some("dynamic") | None => src.size().saturating_sub(seg_off), Some(n) => n.parse().unwrap_or(0), }; - let seg_enc = jstr(seg, "encryption").unwrap_or("aes-xts-plain64").to_string(); + let seg_enc = jstr(seg, "encryption") + .unwrap_or("aes-xts-plain64") + .to_string(); let sector = jnum(seg, "sector_size").unwrap_or(512); let iv_tweak = jnum(seg, "iv_tweak").unwrap_or(0); - let digests = json.get("digests").and_then(|d| d.as_object()).ok_or_else(|| UnlockError::Corrupt("sem digests".into()))?; + let digests = json + .get("digests") + .and_then(|d| d.as_object()) + .ok_or_else(|| UnlockError::Corrupt("sem digests".into()))?; let check = |vk: &[u8]| -> bool { for (_, d) in digests { @@ -325,7 +346,8 @@ fn unlock_v2(src: Arc, secrets: &[Secret]) -> UnlockResult { }; let finish = |vk: Vec| -> UnlockResult { let cipher = parse_cipher(&seg_enc, &vk)?; - let cs = CryptSource::new(src.clone(), cipher, seg_off, seg_len, sector).with_iv_tweak(iv_tweak); + let cs = + CryptSource::new(src.clone(), cipher, seg_off, seg_len, sector).with_iv_tweak(iv_tweak); Ok(Unlocked { source: Arc::new(cs), description: format!("LUKS2 ({seg_enc})"), @@ -338,7 +360,10 @@ fn unlock_v2(src: Arc, secrets: &[Secret]) -> UnlockResult { } } let pws = passwords(secrets); - let slots = json.get("keyslots").and_then(|d| d.as_object()).ok_or_else(|| UnlockError::Corrupt("sem keyslots".into()))?; + let slots = json + .get("keyslots") + .and_then(|d| d.as_object()) + .ok_or_else(|| UnlockError::Corrupt("sem keyslots".into()))?; let mut unsupported: Option = None; for (_, slot) in slots { if jstr(slot, "type") != Some("luks2") { @@ -347,10 +372,20 @@ fn unlock_v2(src: Arc, secrets: &[Secret]) -> UnlockResult { let key_size = jnum(slot, "key_size").unwrap_or(0) as usize; let af = slot.get("af"); let stripes = af.and_then(|a| jnum(a, "stripes")).unwrap_or(4000) as usize; - let af_hash = af.and_then(|a| jstr(a, "hash")).and_then(Hash::parse).unwrap_or(Hash::Sha256); + let af_hash = af + .and_then(|a| jstr(a, "hash")) + .and_then(Hash::parse) + .unwrap_or(Hash::Sha256); let ar = slot.get("area"); - let (Some(a_off), Some(a_size)) = (ar.and_then(|a| jnum(a, "offset")), ar.and_then(|a| jnum(a, "size"))) else { continue }; - let a_enc = ar.and_then(|a| jstr(a, "encryption")).unwrap_or("aes-xts-plain64"); + let (Some(a_off), Some(a_size)) = ( + ar.and_then(|a| jnum(a, "offset")), + ar.and_then(|a| jnum(a, "size")), + ) else { + continue; + }; + let a_enc = ar + .and_then(|a| jstr(a, "encryption")) + .unwrap_or("aes-xts-plain64"); let a_ksize = ar.and_then(|a| jnum(a, "key_size")).unwrap_or(64) as usize; let kdf = slot.get("kdf"); let Some(kdf) = kdf else { continue }; @@ -359,18 +394,31 @@ fn unlock_v2(src: Arc, secrets: &[Secret]) -> UnlockResult { if key_size == 0 || stripes == 0 || km_len as u64 > a_size { continue; } - let Ok(km) = read_vec(src.as_ref(), a_off, km_len) else { continue }; + let Ok(km) = read_vec(src.as_ref(), a_off, km_len) else { + continue; + }; if km.len() < km_len { continue; } for pw in &pws { let dk: Vec = match jstr(kdf, "type") { Some("pbkdf2") => { - let hh = jstr(kdf, "hash").and_then(Hash::parse).unwrap_or(Hash::Sha256); - hh.pbkdf2(pw, &salt, jnum(kdf, "iterations").unwrap_or(1000) as u32, a_ksize) + let hh = jstr(kdf, "hash") + .and_then(Hash::parse) + .unwrap_or(Hash::Sha256); + hh.pbkdf2( + pw, + &salt, + jnum(kdf, "iterations").unwrap_or(1000) as u32, + a_ksize, + ) } Some(t @ ("argon2i" | "argon2id")) => { - let alg = if t == "argon2i" { argon2::Algorithm::Argon2i } else { argon2::Algorithm::Argon2id }; + let alg = if t == "argon2i" { + argon2::Algorithm::Argon2i + } else { + argon2::Algorithm::Argon2id + }; let params = match argon2::Params::new( jnum(kdf, "memory").unwrap_or(65536) as u32, jnum(kdf, "time").unwrap_or(4) as u32, @@ -384,7 +432,10 @@ fn unlock_v2(src: Arc, secrets: &[Secret]) -> UnlockResult { } }; let mut out = vec![0u8; a_ksize]; - if argon2::Argon2::new(alg, argon2::Version::V0x13, params).hash_password_into(pw, &salt, &mut out).is_err() { + if argon2::Argon2::new(alg, argon2::Version::V0x13, params) + .hash_password_into(pw, &salt, &mut out) + .is_err() + { continue; } out @@ -413,6 +464,11 @@ fn unlock_v2(src: Arc, secrets: &[Secret]) -> UnlockResult { } /// Atalho: abre uma janela da origem como LUKS. -pub fn unlock_window(base: Arc, start: u64, len: u64, secrets: &[Secret]) -> UnlockResult { +pub fn unlock_window( + base: Arc, + start: u64, + len: u64, + secrets: &[Secret], +) -> UnlockResult { unlock(Arc::new(SubSource::new(base, start, len)), secrets) } diff --git a/crates/guara-core/src/crypt/mod.rs b/crates/guara-core/src/crypt/mod.rs index 61bd1af..3ea965e 100644 --- a/crates/guara-core/src/crypt/mod.rs +++ b/crates/guara-core/src/crypt/mod.rs @@ -98,10 +98,20 @@ impl Iv { /// Cifra aplicada setor a setor. #[derive(Clone)] pub enum SectorCipher { - Xts { k1: AesAny, k2: AesAny, iv: Iv }, - Cbc { key: AesAny, iv: Iv }, + Xts { + k1: AesAny, + k2: AesAny, + iv: Iv, + }, + Cbc { + key: AesAny, + iv: Iv, + }, /// BitLocker AES-CBC com difusor Elephant (chave de dados + chave de ajuste). - Elephant { key: AesAny, tweak: AesAny }, + Elephant { + key: AesAny, + tweak: AesAny, + }, } fn gf_double(t: &mut [u8; 16]) { @@ -200,7 +210,13 @@ pub struct Patch { } impl CryptSource { - pub fn new(inner: Arc, cipher: SectorCipher, data_offset: u64, len: u64, sector: u64) -> Self { + pub fn new( + inner: Arc, + cipher: SectorCipher, + data_offset: u64, + len: u64, + sector: u64, + ) -> Self { let sector = sector.max(512); Self { inner, @@ -245,7 +261,8 @@ impl CryptSource { } crate::source::read_exact_at(self.inner.as_ref(), self.data_offset + rel, out)?; if rel < self.plain_from { - self.cipher.decrypt_sector(out, rel / 512 + self.iv_tweak, rel); + self.cipher + .decrypt_sector(out, rel / 512 + self.iv_tweak, rel); } Ok(()) } @@ -342,5 +359,7 @@ pub fn from_hex(s: &str) -> Option> { if s.len() % 2 != 0 { return None; } - (0..s.len() / 2).map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()).collect() + (0..s.len() / 2) + .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()) + .collect() } diff --git a/crates/guara-core/src/defrag.rs b/crates/guara-core/src/defrag.rs index fea0a95..57453a6 100644 --- a/crates/guara-core/src/defrag.rs +++ b/crates/guara-core/src/defrag.rs @@ -26,7 +26,9 @@ pub struct ClusterIndex { impl ClusterIndex { pub fn new(size: u64) -> Self { - Self { bits: vec![0; (size / CLUSTER / 64 + 1) as usize] } + Self { + bits: vec![0; (size / CLUSTER / 64 + 1) as usize], + } } pub fn set(&mut self, off: u64) { let c = (off / CLUSTER) as usize; @@ -36,7 +38,10 @@ impl ClusterIndex { } pub fn get(&self, off: u64) -> bool { let c = (off / CLUSTER) as usize; - self.bits.get(c / 64).map(|w| w >> (c % 64) & 1 == 1).unwrap_or(false) + self.bits + .get(c / 64) + .map(|w| w >> (c % 64) & 1 == 1) + .unwrap_or(false) } /// Examina um trecho lido e marca os clusters candidatos. pub fn scan_chunk(&mut self, abs: u64, buf: &[u8]) { @@ -52,7 +57,14 @@ impl ClusterIndex { } pub fn iter_range(&self, from: u64, to: u64) -> impl Iterator + '_ { let (a, b) = (from.div_ceil(CLUSTER), to / CLUSTER); - (a..b).filter(move |&c| self.bits.get(c as usize / 64).map(|w| w >> (c % 64) & 1 == 1).unwrap_or(false)).map(|c| c * CLUSTER) + (a..b) + .filter(move |&c| { + self.bits + .get(c as usize / 64) + .map(|w| w >> (c % 64) & 1 == 1) + .unwrap_or(false) + }) + .map(|c| c * CLUSTER) } } @@ -84,7 +96,10 @@ pub struct Pool<'a> { impl Pool<'_> { fn free_at(&self, off: u64, len: u64) -> bool { - !self.used.iter().any(|u| off < u.end() && off + len > u.offset) + !self + .used + .iter() + .any(|u| off < u.end() && off + len > u.offset) } } @@ -103,14 +118,26 @@ pub enum Analysis { /// Procura FF D9 logo depois do último MCU (até 64 bytes de preenchimento). fn eoi_after(data: &[u8], pos: usize) -> Option { let end = (pos + 64).min(data.len().saturating_sub(1)); - (pos.saturating_sub(2)..end).find(|&i| data[i] == 0xFF && data[i + 1] == 0xD9).map(|i| i + 2) + (pos.saturating_sub(2)..end) + .find(|&i| data[i] == 0xFF && data[i + 1] == 0xD9) + .map(|i| i + 2) } /// Valida o JPEG que começa em `off` e, se estiver quebrado, tenta recompô-lo. -pub fn analyze_jpeg(src: &dyn Source, off: u64, hit_len: u64, pool: &Pool, allow_rebuild: bool) -> Analysis { +pub fn analyze_jpeg( + src: &dyn Source, + off: u64, + hit_len: u64, + pool: &Pool, + allow_rebuild: bool, +) -> Analysis { let read_len = (hit_len as usize).clamp(4096, MAX_FILE); - let Ok(buf) = read_vec(src, off, read_len) else { return Analysis::Unsupported }; - let Some(h) = jpegscan::parse_header(&buf) else { return Analysis::Unsupported }; + let Ok(buf) = read_vec(src, off, read_len) else { + return Analysis::Unsupported; + }; + let Some(h) = jpegscan::parse_header(&buf) else { + return Analysis::Unsupported; + }; let d = jpegscan::decode(&h, &buf, State::start(&h), u32::MAX, 512); let disc = jpegscan::first_discontinuity(&h, &d.dc); if d.stop == Stop::Done && disc.is_none() { @@ -123,12 +150,20 @@ pub fn analyze_jpeg(src: &dyn Source, off: u64, hit_len: u64, pool: &Pool, allow Stop::Truncated | Stop::Done => buf.len(), }; if let Some(m) = disc { - let bad = d.snaps.iter().rev().find(|s| (s.mcu as usize) <= m).map(|s| s.pos).unwrap_or(h.scan_start); + let bad = d + .snaps + .iter() + .rev() + .find(|s| (s.mcu as usize) <= m) + .map(|s| s.pos) + .unwrap_or(h.scan_start); stop_pos = stop_pos.min(bad); } let cl = CLUSTER as usize; if off % CLUSTER != 0 || !allow_rebuild { - return Analysis::Broken { good_len: (stop_pos / 512 * 512).max(512) as u64 }; + return Analysis::Broken { + good_len: (stop_pos / 512 * 512).max(512) as u64, + }; } let mut pieces: Vec<(u64, usize)> = vec![]; @@ -143,7 +178,9 @@ pub fn analyze_jpeg(src: &dyn Source, off: u64, hit_len: u64, pool: &Pool, allow let mut best: Option<(usize, u64, State, f64)> = None; let mut probes = 0usize; for back in 0..4usize { - let Some(bc_rel) = top.checked_sub(back * cl).filter(|&b| b > 0) else { break }; + let Some(bc_rel) = top.checked_sub(back * cl).filter(|&b| b > 0) else { + break; + }; let bc = frag_start + bc_rel; let Some(snap) = snaps .iter() @@ -155,24 +192,32 @@ pub fn analyze_jpeg(src: &dyn Source, off: u64, hit_len: u64, pool: &Pool, allow continue; }; let mut plane = jpegscan::Plane::new(&h); - let _ = jpegscan::decode_plane(&h, &joined[..bc], State::start(&h), u32::MAX, &mut plane); + let _ = + jpegscan::decode_plane(&h, &joined[..bc], State::start(&h), u32::MAX, &mut plane); let lo = off.saturating_sub(WINDOW_BEFORE); let hi = cur_base + bc_rel as u64 + WINDOW_AFTER; let pre = jpegscan::decode(&h, &joined[..bc], State::start(&h), u32::MAX, 0); - let bpm_prefix = (pre.state.pos.saturating_sub(h.scan_start)) as f64 / pre.dc.len().max(1) as f64; + let bpm_prefix = + (pre.state.pos.saturating_sub(h.scan_start)) as f64 / pre.dc.len().max(1) as f64; let mut ranked: Vec<(f64, u64)> = vec![]; for q in pool.index.iter_range(lo, hi) { if q >= cur_base && q < cur_base + bc_rel as u64 + CLUSTER { continue; } - if !pool.free_at(q, CLUSTER) || pieces.iter().any(|&(po, pl)| q < po + pl as u64 && q + CLUSTER > po) { + if !pool.free_at(q, CLUSTER) + || pieces + .iter() + .any(|&(po, pl)| q < po + pl as u64 && q + CLUSTER > po) + { continue; } probes += 1; if probes > MAX_PROBES { break; } - if let Some(sc) = probe_candidate(src, &h, &joined[..bc], &plane, &snap, q, bpm_prefix) { + if let Some(sc) = + probe_candidate(src, &h, &joined[..bc], &plane, &snap, q, bpm_prefix) + { ranked.push((sc, q)); } } @@ -208,7 +253,7 @@ pub fn analyze_jpeg(src: &dyn Source, off: u64, hit_len: u64, pool: &Pool, allow } } } - if best.as_ref().map_or(false, |b| b.3 == 0.0) { + if best.as_ref().is_some_and(|b| b.3 == 0.0) { break; } } @@ -231,7 +276,13 @@ pub fn analyze_jpeg(src: &dyn Source, off: u64, hit_len: u64, pool: &Pool, allow }; let all = jpegscan::decode(&h, &joined, State::start(&h), u32::MAX, 0); if let Some(m) = jpegscan::first_discontinuity(&h, &all.dc) { - let bad = d2.snaps.iter().rev().find(|s| (s.mcu as usize) <= m).map(|s| s.pos).unwrap_or(bc); + let bad = d2 + .snaps + .iter() + .rev() + .find(|s| (s.mcu as usize) <= m) + .map(|s| s.pos) + .unwrap_or(bc); if bad > bc { sp = sp.min(bad); } @@ -241,8 +292,11 @@ pub fn analyze_jpeg(src: &dyn Source, off: u64, hit_len: u64, pool: &Pool, allow frag_start = bc; suspect = sp.max(bc + cl); } - let good = pieces.iter().map(|p| p.1).sum::() + suspect.saturating_sub(frag_start).min(cl * 4) / 512 * 512; - Analysis::Broken { good_len: (stop_pos.min(good.max(512)) / 512 * 512).max(512) as u64 } + let good = pieces.iter().map(|p| p.1).sum::() + + suspect.saturating_sub(frag_start).min(cl * 4) / 512 * 512; + Analysis::Broken { + good_len: (stop_pos.min(good.max(512)) / 512 * 512).max(512) as u64, + } } fn finish(pieces: Vec<(u64, usize)>) -> Analysis { @@ -262,7 +316,15 @@ fn finish(pieces: Vec<(u64, usize)>) -> Analysis { /// Pontuação (menor é melhor) de `q` como continuação de `prefix`: diferença média de pixels /// nas bordas entre os MCUs novos e seus vizinhos. Natural: poucos níveis de cinza; lixo: dezenas. -fn probe_candidate(src: &dyn Source, h: &Header, prefix: &[u8], plane: &jpegscan::Plane, snap: &State, q: u64, bpm_prefix: f64) -> Option { +fn probe_candidate( + src: &dyn Source, + h: &Header, + prefix: &[u8], + plane: &jpegscan::Plane, + snap: &State, + q: u64, + bpm_prefix: f64, +) -> Option { let more = read_vec(src, q, PROBE).ok()?; if more.len() < 1024 { return None; @@ -326,7 +388,11 @@ fn probe_candidate(src: &dyn Source, h: &Header, prefix: &[u8], plane: &jpegscan // Bytes por MCU depois da emenda, comparados com os do trecho anterior. let after = d.dc.len() as f64 - (seam.saturating_sub(snap.mcu)) as f64; let used = d.state.pos.saturating_sub(prefix.len()) as f64; - let ratio = if after > 4.0 && used > 0.0 { (used / after) / bpm_prefix.max(0.5) } else { 1.0 }; + let ratio = if after > 4.0 && used > 0.0 { + (used / after) / bpm_prefix.max(0.5) + } else { + 1.0 + }; let sc = sum / n as f64 + 20.0 * ratio.ln().abs(); Some(sc) } diff --git a/crates/guara-core/src/ewf.rs b/crates/guara-core/src/ewf.rs index 68b0944..f8a8c89 100644 --- a/crates/guara-core/src/ewf.rs +++ b/crates/guara-core/src/ewf.rs @@ -55,7 +55,10 @@ fn pread_exact(f: &File, buf: &mut [u8], mut off: u64) -> io::Result<()> { while done < buf.len() { let n = pread(f, &mut buf[done..], off)?; if n == 0 { - return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "segmento EWF truncado")); + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "segmento EWF truncado", + )); } done += n; off += n as u64; @@ -83,7 +86,9 @@ enum Codec { #[derive(Clone, Copy, Debug)] enum Kind { /// Dados guardados como estão; `checksum` indica 4 bytes de Adler-32 no fim. - Stored { checksum: bool }, + Stored { + checksum: bool, + }, Packed(Codec), /// Chunk inteiro repetindo estes 8 bytes (EnCase 7 "empty block"). Pattern([u8; 8]), @@ -141,7 +146,11 @@ pub fn next_segment_ext(ext: &str) -> Option { format!("{}AA", first as char) } } else { - let mut c = [first, rest.as_bytes()[0].to_ascii_uppercase(), rest.as_bytes()[1].to_ascii_uppercase()]; + let mut c = [ + first, + rest.as_bytes()[0].to_ascii_uppercase(), + rest.as_bytes()[1].to_ascii_uppercase(), + ]; let mut i = 2; loop { if c[i] < b'Z' { @@ -206,13 +215,22 @@ impl EwfSource { } } - fn finish(files: Vec, chunks: Vec, mut info: EwfInfo, size_hint: u64) -> Result { + fn finish( + files: Vec, + chunks: Vec, + mut info: EwfInfo, + size_hint: u64, + ) -> Result { if chunks.is_empty() { return Err(Error::msg("imagem EWF sem tabela de chunks")); } let chunk_size = info.chunk_size; let max = chunks.len() as u64 * chunk_size; - let size = if size_hint > 0 { size_hint.min(max) } else { max }; + let size = if size_hint > 0 { + size_hint.min(max) + } else { + max + }; info.segments = files.len(); Ok(Self { files, @@ -284,7 +302,11 @@ impl EwfSource { .collect(); for i in 0..n { let (off, comp) = offs[i]; - let end = if i + 1 < n { offs[i + 1].0 } else { sectors_end }; + let end = if i + 1 < n { + offs[i + 1].0 + } else { + sectors_end + }; if end <= off { return Err(Error::msg("tabela EWF inconsistente")); } @@ -396,11 +418,25 @@ impl EwfSource { let kind = if fl & 4 != 0 { Kind::Pattern(off.to_le_bytes()) } else if fl & 1 != 0 { - Kind::Packed(if method == Codec::Raw { Codec::Deflate } else { method }) + Kind::Packed(if method == Codec::Raw { + Codec::Deflate + } else { + method + }) } else { - Kind::Stored { checksum: fl & 2 != 0 } + Kind::Stored { + checksum: fl & 2 != 0, + } }; - slots.insert(first + i as u64, Chunk { seg: si as u32, off, len, kind }); + slots.insert( + first + i as u64, + Chunk { + seg: si as u32, + off, + len, + kind, + }, + ); } } } @@ -432,7 +468,9 @@ impl EwfSource { let mut chunks = Vec::with_capacity(slots.len()); for (i, (k, c)) in slots.into_iter().enumerate() { if k != i as u64 { - return Err(Error::msg("segmento Ex01 ausente: sequência de chunks com lacuna")); + return Err(Error::msg( + "segmento Ex01 ausente: sequência de chunks com lacuna", + )); } chunks.push(c); } @@ -457,10 +495,19 @@ impl EwfSource { pread_exact(&self.files[c.seg as usize], &mut raw, c.off)?; let mut v = Vec::with_capacity(want); let r: io::Result = match codec { - Codec::Bzip2 => bzip2::read::BzDecoder::new(&raw[..]).take(want as u64).read_to_end(&mut v), - _ => flate2::read::ZlibDecoder::new(&raw[..]).take(want as u64).read_to_end(&mut v), + Codec::Bzip2 => bzip2::read::BzDecoder::new(&raw[..]) + .take(want as u64) + .read_to_end(&mut v), + _ => flate2::read::ZlibDecoder::new(&raw[..]) + .take(want as u64) + .read_to_end(&mut v), }; - r.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("chunk EWF {idx} corrompido: {e}")))?; + r.map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("chunk EWF {idx} corrompido: {e}"), + ) + })?; v } }; @@ -529,7 +576,9 @@ impl Source for EwfSource { fn inflate_all(raw: &[u8]) -> Vec { let mut v = vec![]; if raw.len() > 2 && raw[0] == 0x78 { - let _ = flate2::read::ZlibDecoder::new(raw).take(1 << 24).read_to_end(&mut v); + let _ = flate2::read::ZlibDecoder::new(raw) + .take(1 << 24) + .read_to_end(&mut v); if !v.is_empty() { return v; } @@ -538,8 +587,13 @@ fn inflate_all(raw: &[u8]) -> Vec { } fn utf16_text(b: &[u8]) -> String { - let u: Vec = b.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect(); - String::from_utf16_lossy(&u).trim_start_matches('\u{feff}').to_string() + let u: Vec = b + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); + String::from_utf16_lossy(&u) + .trim_start_matches('\u{feff}') + .to_string() } fn header_key_name(k: &str) -> &str { @@ -594,9 +648,18 @@ fn parse_v2_text(text: &str, info: &mut EwfInfo, total_bytes: &mut u64, ty: u32) let spc: u64 = v.parse().unwrap_or(64); info.chunk_size = spc * 512; } - (2, "tb") | (2, "gr") | (_, "dt") | (_, "pid") | (_, "rs") | (_, "ls") | (_, "ph") | (_, "tt") | (_, "at") => {} + (2, "tb") + | (2, "gr") + | (_, "dt") + | (_, "pid") + | (_, "rs") + | (_, "ls") + | (_, "ph") + | (_, "tt") + | (_, "at") => {} (2, key) => { - info.fields.insert(header_key_name(key).to_string(), v.to_string()); + info.fields + .insert(header_key_name(key).to_string(), v.to_string()); } _ => {} } @@ -728,7 +791,12 @@ fn utc_now() -> String { let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = if m <= 2 { y + 1 } else { y }; - format!("{y} {m} {d} {} {} {}", rem / 3600, rem % 3600 / 60, rem % 60) + format!( + "{y} {m} {d} {} {} {}", + rem / 3600, + rem % 3600 / 60, + rem % 60 + ) } /// Cria `base.E01` (e `.E02`... se houver divisão) a partir de `src`. @@ -772,7 +840,8 @@ pub fn write_e01( // O segmento está cheio? (reserva espaço para as tabelas e os hashes finais) let full = |pos: u64, entries: u64| { - opts.segment_size > 0 && pos + chunk_size + 2 * (entries * 4 + 110) + 1024 >= opts.segment_size + opts.segment_size > 0 + && pos + chunk_size + 2 * (entries * 4 + 110) + 1024 >= opts.segment_size }; let result = (|| -> Result<()> { loop { @@ -781,7 +850,9 @@ pub fn write_e01( .write(true) .create_new(true) .open(&path) - .map_err(|e| Error::msg(format!("não foi possível criar {}: {e}", path.display())))?; + .map_err(|e| { + Error::msg(format!("não foi possível criar {}: {e}", path.display())) + })?; crate::owner::give_to_user(&path); created.push(path); let mut w = SegWriter { f, pos: 0 }; @@ -794,7 +865,15 @@ pub fn write_e01( let hdr = zlib(&header_text(opts, &now), 9); w.section("header", &hdr, false)?; w.section("header2", &hdr, false)?; - w.section("volume", &volume_data(opts, chunks_total as u32, total.div_ceil(opts.bytes_per_sector as u64)), false)?; + w.section( + "volume", + &volume_data( + opts, + chunks_total as u32, + total.div_ceil(opts.bytes_per_sector as u64), + ), + false, + )?; } // Grupos "sectors" + "table" + "table2" até estourar o limite do segmento. loop { @@ -821,7 +900,9 @@ pub fn write_e01( let mut o = 0; while o < want { let n = 512.min(want - o); - if crate::source::read_exact_at(src, off + o as u64, &mut b[o..o + n]).is_err() { + if crate::source::read_exact_at(src, off + o as u64, &mut b[o..o + n]) + .is_err() + { b[o..o + n].fill(0); match rep.bad_ranges.last_mut() { Some(l) if l.0 + l.1 == off + o as u64 => l.1 += n as u64, @@ -833,7 +914,11 @@ pub fn write_e01( } hasher.update(b); let rel = w.pos - base_off; - let packed = if opts.compression > 0 { zlib(b, opts.compression) } else { vec![] }; + let packed = if opts.compression > 0 { + zlib(b, opts.compression) + } else { + vec![] + }; if opts.compression > 0 && packed.len() < want { entries.push(rel as u32 | 0x8000_0000); w.put(&packed)?; @@ -888,8 +973,12 @@ pub fn write_e01( if chunk_idx >= chunks_total { // Segmento final: hashes e "done". let h = std::mem::take(&mut hasher).finalize(); - let md5: Vec = (0..16).map(|i| u8::from_str_radix(&h.md5[i * 2..i * 2 + 2], 16).unwrap()).collect(); - let sha1: Vec = (0..20).map(|i| u8::from_str_radix(&h.sha1[i * 2..i * 2 + 2], 16).unwrap()).collect(); + let md5: Vec = (0..16) + .map(|i| u8::from_str_radix(&h.md5[i * 2..i * 2 + 2], 16).unwrap()) + .collect(); + let sha1: Vec = (0..20) + .map(|i| u8::from_str_radix(&h.sha1[i * 2..i * 2 + 2], 16).unwrap()) + .collect(); let mut hs = md5.clone(); hs.extend_from_slice(&[0; 16]); let c = adler32(&hs); @@ -924,6 +1013,9 @@ pub fn write_e01( if rep.cancelled { rep.bytes = chunk_idx * chunk_size; } - crate::owner::write_owned(&base.with_extension("map.json"), serde_json::to_vec_pretty(&rep)?)?; + crate::owner::write_owned( + &base.with_extension("map.json"), + serde_json::to_vec_pretty(&rep)?, + )?; Ok(rep) } diff --git a/crates/guara-core/src/formats.rs b/crates/guara-core/src/formats.rs index 1f1978d..2b168d5 100644 --- a/crates/guara-core/src/formats.rs +++ b/crates/guara-core/src/formats.rs @@ -7,28 +7,64 @@ use crate::source::{read_vec, Source}; use crate::types::Confidence; fn u16le(b: &[u8], o: usize) -> u64 { - b.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]]) as u64).unwrap_or(0) + b.get(o..o + 2) + .map(|s| u16::from_le_bytes([s[0], s[1]]) as u64) + .unwrap_or(0) } fn u32le(b: &[u8], o: usize) -> u64 { - b.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap()) as u64).unwrap_or(0) + b.get(o..o + 4) + .map(|s| u32::from_le_bytes(s.try_into().unwrap()) as u64) + .unwrap_or(0) } fn u64le(b: &[u8], o: usize) -> u64 { - b.get(o..o + 8).map(|s| u64::from_le_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 8) + .map(|s| u64::from_le_bytes(s.try_into().unwrap())) + .unwrap_or(0) } fn u32be(b: &[u8], o: usize) -> u64 { - b.get(o..o + 4).map(|s| u32::from_be_bytes(s.try_into().unwrap()) as u64).unwrap_or(0) + b.get(o..o + 4) + .map(|s| u32::from_be_bytes(s.try_into().unwrap()) as u64) + .unwrap_or(0) } fn u16be(b: &[u8], o: usize) -> u64 { - b.get(o..o + 2).map(|s| u16::from_be_bytes([s[0], s[1]]) as u64).unwrap_or(0) + b.get(o..o + 2) + .map(|s| u16::from_be_bytes([s[0], s[1]]) as u64) + .unwrap_or(0) } fn hit(len: u64, ext: &'static str, confidence: Confidence) -> Option { - Some(Hit { len, ext, confidence }) + Some(Hit { + len, + ext, + confidence, + }) } /// Primeiros bytes que disparam os detectores deste módulo. pub fn might_start(b0: u8) -> bool { - matches!(b0, 0x1F | b'B' | 0x1A | b'O' | b'R' | 0x7F | b'I' | b'M' | 0x00 | b'r' | b'E' | 0x47 | b'!' | b'F' | b'C' | b'Z' | 0x30 | b'{' | 0x28 | 0xD4 | 0x0A) + matches!( + b0, + 0x1F | b'B' + | 0x1A + | b'O' + | b'R' + | 0x7F + | b'I' + | b'M' + | 0x00 + | b'r' + | b'E' + | 0x47 + | b'!' + | b'F' + | b'C' + | b'Z' + | 0x30 + | b'{' + | 0x28 + | 0xD4 + | 0x0A + ) } /// Tenta os formatos adicionais. `h` são os primeiros bytes (>= 12) lidos em `off`. @@ -36,7 +72,10 @@ pub fn detect(src: &dyn Source, off: u64, h: &[u8], max: u64) -> Option { if h.starts_with(&[0x1F, 0x8B, 0x08]) { return gzip(src, off, max); } - if h.starts_with(b"BZh") && (b'1'..=b'9').contains(&h[3]) && h[4..10] == [0x31, 0x41, 0x59, 0x26, 0x53, 0x59] { + if h.starts_with(b"BZh") + && (b'1'..=b'9').contains(&h[3]) + && h[4..10] == [0x31, 0x41, 0x59, 0x26, 0x53, 0x59] + { return bzip2(src, off, max); } if h.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) { @@ -68,7 +107,9 @@ pub fn detect(src: &dyn Source, off: u64, h: &[u8], max: u64) -> Option { } if h.starts_with(b"MSCF") { let len = u32le(h, 8); - return (len > 36 && len <= max).then(|| ()).and_then(|_| hit(len, "cab", Confidence::High)); + return (len > 36 && len <= max) + .then_some(()) + .and_then(|_| hit(len, "cab", Confidence::High)); } if h.starts_with(b"!\n") { return ar(src, off, max); @@ -96,7 +137,9 @@ pub fn detect(src: &dyn Source, off: u64, h: &[u8], max: u64) -> Option { } if h.starts_with(b"FORM") && matches!(&h[8..12], b"AIFF" | b"AIFC") { let len = u32be(h, 4) + 8; - return (len <= max).then(|| ()).and_then(|_| hit(len, "aif", Confidence::High)); + return (len <= max) + .then_some(()) + .and_then(|_| hit(len, "aif", Confidence::High)); } if h.starts_with(&[0x28, 0xB5, 0x2F, 0xFD]) { return zstd(src, off, max); @@ -113,7 +156,13 @@ pub fn detect(src: &dyn Source, off: u64, h: &[u8], max: u64) -> Option { // -------------------------------------------------------------- compactadores /// Alimenta um descompactador com blocos de `src` até o fim do fluxo; devolve (bytes lidos, saída crc32 opcional). -fn inflate_len(src: &dyn Source, start: u64, max: u64, raw: bool, zlib_header: bool) -> Option<(u64, u32, u64)> { +fn inflate_len( + src: &dyn Source, + start: u64, + max: u64, + raw: bool, + zlib_header: bool, +) -> Option<(u64, u32, u64)> { let mut d = flate2::Decompress::new(zlib_header && !raw); let mut pos = start; let mut out = vec![0u8; 1 << 16]; @@ -132,13 +181,19 @@ fn inflate_len(src: &dyn Source, start: u64, max: u64, raw: bool, zlib_header: b while consumed < n { let before_in = d.total_in(); let before_out = d.total_out(); - let status = d.decompress(&buf[consumed..n], &mut out, flate2::FlushDecompress::None).ok()?; + let status = d + .decompress(&buf[consumed..n], &mut out, flate2::FlushDecompress::None) + .ok()?; let used = (d.total_in() - before_in) as usize; let produced = (d.total_out() - before_out) as usize; for &b in &out[..produced] { crc ^= b as u32; for _ in 0..8 { - crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 }; + crc = if crc & 1 != 0 { + (crc >> 1) ^ 0xEDB8_8320 + } else { + crc >> 1 + }; } } total_out += produced as u64; @@ -178,7 +233,15 @@ fn gzip(src: &dyn Source, off: u64, max: u64) -> Option { return None; } let ok = u32le(&trailer, 0) as u32 == crc && u32le(&trailer, 4) == out_len & 0xFFFF_FFFF; - hit(p as u64 + consumed + 8, "gz", if ok { Confidence::High } else { Confidence::Medium }) + hit( + p as u64 + consumed + 8, + "gz", + if ok { + Confidence::High + } else { + Confidence::Medium + }, + ) } fn bzip2(src: &dyn Source, off: u64, max: u64) -> Option { @@ -286,8 +349,13 @@ fn matroska(src: &dyn Source, off: u64, max: u64) -> Option { let h = read_vec(src, off, 4096).ok()?; let (hs, hl, _) = vint(&h[4..])?; let mut p = 4 + hl + hs as usize; - let doctype = String::from_utf8_lossy(&h[4 + hl..(4 + hl + hs as usize).min(h.len())]).to_string(); - let ext = if doctype.contains("webm") { "webm" } else { "mkv" }; + let doctype = + String::from_utf8_lossy(&h[4 + hl..(4 + hl + hs as usize).min(h.len())]).to_string(); + let ext = if doctype.contains("webm") { + "webm" + } else { + "mkv" + }; if h.get(p..p + 4)? != [0x18, 0x53, 0x80, 0x67] { return None; } @@ -307,9 +375,13 @@ fn matroska(src: &dyn Source, off: u64, max: u64) -> Option { if e.len() < 6 { break; } - let Some((id, idl, _)) = (e[0] >= 0x10).then(|| vint_id(&e)).flatten() else { break }; + let Some((id, idl, _)) = (e[0] >= 0x10).then(|| vint_id(&e)).flatten() else { + break; + }; let _ = id; - let Some((size, sl2, unk)) = vint(&e[idl..]) else { break }; + let Some((size, sl2, unk)) = vint(&e[idl..]) else { + break; + }; if unk { break; } @@ -370,7 +442,17 @@ fn ogg(src: &dyn Source, off: u64, max: u64) -> Option { break; } } - (pages >= 2).then(|| ()).and_then(|_| hit(pos - off, "ogg", if eos { Confidence::High } else { Confidence::Low })) + (pages >= 2).then_some(()).and_then(|_| { + hit( + pos - off, + "ogg", + if eos { + Confidence::High + } else { + Confidence::Low + }, + ) + }) } fn mpegts(src: &dyn Source, off: u64, max: u64) -> Option { @@ -396,7 +478,9 @@ fn mpegts(src: &dyn Source, off: u64, max: u64) -> Option { } } let len = n_packets * 188; - (n_packets >= 16).then(|| ()).and_then(|_| hit(len, "ts", Confidence::Medium)) + (n_packets >= 16) + .then_some(()) + .and_then(|_| hit(len, "ts", Confidence::Medium)) } fn flv(src: &dyn Source, off: u64, max: u64) -> Option { @@ -420,14 +504,16 @@ fn flv(src: &dyn Source, off: u64, max: u64) -> Option { return None; } } - (tags > 3).then(|| ()).and_then(|_| hit(pos + 4 - off, "flv", Confidence::Medium)) + (tags > 3) + .then_some(()) + .and_then(|_| hit(pos + 4 - off, "flv", Confidence::Medium)) } fn asf(src: &dyn Source, off: u64, max: u64) -> Option { let h = read_vec(src, off, 30).ok()?; let hdr_size = u64le(&h, 16); let n_objs = u32le(&h, 24); - if hdr_size < 30 || hdr_size > 1 << 24 { + if !(30..=1 << 24).contains(&hdr_size) { return None; } let all = read_vec(src, off + 30, (hdr_size - 30) as usize).ok()?; @@ -437,7 +523,12 @@ fn asf(src: &dyn Source, off: u64, max: u64) -> Option { break; } let size = u64le(&all, p + 16) as usize; - if all[p..p + 16] == [0xA1, 0xDC, 0xAB, 0x8C, 0x47, 0xA9, 0xCF, 0x11, 0x8E, 0xE4, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65] { + if all[p..p + 16] + == [ + 0xA1, 0xDC, 0xAB, 0x8C, 0x47, 0xA9, 0xCF, 0x11, 0x8E, 0xE4, 0x00, 0xC0, 0x0C, 0x20, + 0x53, 0x65, + ] + { let fsize = u64le(&all, p + 40); if fsize >= hdr_size && fsize <= max { return hit(fsize, "wmv", Confidence::High); @@ -596,7 +687,9 @@ fn ar(src: &dyn Source, off: u64, max: u64) -> Option { return None; } } - (members >= 2).then(|| ()).and_then(|_| hit(pos - off, "deb", Confidence::Medium)) + (members >= 2) + .then_some(()) + .and_then(|_| hit(pos - off, "deb", Confidence::Medium)) } fn swf(src: &dyn Source, off: u64, max: u64) -> Option { @@ -606,11 +699,21 @@ fn swf(src: &dyn Source, off: u64, max: u64) -> Option { return None; } if h[0] == b'F' { - return (len > 21 && len <= max).then(|| ()).and_then(|_| hit(len, "swf", Confidence::High)); + return (len > 21 && len <= max) + .then_some(()) + .and_then(|_| hit(len, "swf", Confidence::High)); } // CWS: corpo em zlib a partir do byte 8 let (consumed, _, out) = inflate_len(src, off + 8, max, false, true)?; - hit(8 + consumed, "swf", if out + 8 == len { Confidence::High } else { Confidence::Medium }) + hit( + 8 + consumed, + "swf", + if out + 8 == len { + Confidence::High + } else { + Confidence::Medium + }, + ) } // ---------------------------------------------------------- binários e dados @@ -635,9 +738,23 @@ fn elf(src: &dyn Source, off: u64, max: u64) -> Option { return None; } let (shoff, shentsize, shnum, phoff, phentsize, phnum) = if is64 { - (rd(40, 8), rd(58, 2), rd(60, 2), rd(32, 8), rd(54, 2), rd(56, 2)) + ( + rd(40, 8), + rd(58, 2), + rd(60, 2), + rd(32, 8), + rd(54, 2), + rd(56, 2), + ) } else { - (rd(32, 4), rd(46, 2), rd(48, 2), rd(28, 4), rd(42, 2), rd(44, 2)) + ( + rd(32, 4), + rd(46, 2), + rd(48, 2), + rd(28, 4), + rd(42, 2), + rd(44, 2), + ) }; let mut end = shoff + shentsize * shnum; // Segmentos de programa podem ir além (binários sem tabela de seções). @@ -653,11 +770,27 @@ fn elf(src: &dyn Source, off: u64, max: u64) -> Option { } v }; - let (po, fs) = if is64 { (rd2(8, 8), rd2(32, 8)) } else { (rd2(4, 4), rd2(16, 4)) }; + let (po, fs) = if is64 { + (rd2(8, 8), rd2(32, 8)) + } else { + (rd2(4, 4), rd2(16, 4)) + }; end = end.max(po + fs); } } - (end > 64 && end <= max.min(4 << 30)).then(|| ()).and_then(|_| hit(end, "elf", if shoff > 0 { Confidence::High } else { Confidence::Medium })) + (end > 64 && end <= max.min(4 << 30)) + .then_some(()) + .and_then(|_| { + hit( + end, + "elf", + if shoff > 0 { + Confidence::High + } else { + Confidence::Medium + }, + ) + }) } fn tiff(src: &dyn Source, off: u64, max: u64) -> Option { @@ -665,11 +798,22 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option { let le = h[0] == b'I'; let r16 = |b: &[u8], o: usize| -> u64 { let s = b.get(o..o + 2).unwrap_or(&[0, 0]); - if le { u16::from_le_bytes([s[0], s[1]]) as u64 } else { u16::from_be_bytes([s[0], s[1]]) as u64 } + if le { + u16::from_le_bytes([s[0], s[1]]) as u64 + } else { + u16::from_be_bytes([s[0], s[1]]) as u64 + } }; let r32 = |b: &[u8], o: usize| -> u64 { - let s: [u8; 4] = b.get(o..o + 4).and_then(|x| x.try_into().ok()).unwrap_or([0; 4]); - if le { u32::from_le_bytes(s) as u64 } else { u32::from_be_bytes(s) as u64 } + let s: [u8; 4] = b + .get(o..o + 4) + .and_then(|x| x.try_into().ok()) + .unwrap_or([0; 4]); + if le { + u32::from_le_bytes(s) as u64 + } else { + u32::from_be_bytes(s) as u64 + } }; let mut ifd = r32(&h, 4); let mut end = 8u64; @@ -714,7 +858,11 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option { if count == 0 || count > 1 << 20 { return out; } - let bytes: Vec = if total <= 4 { en[8..8 + total as usize].to_vec() } else { read_vec(src, off + value_off, total as usize).unwrap_or_default() }; + let bytes: Vec = if total <= 4 { + en[8..8 + total as usize].to_vec() + } else { + read_vec(src, off + value_off, total as usize).unwrap_or_default() + }; for k in 0..count as usize { let v = match sz { 2 => r16(&bytes, k * 2), @@ -726,8 +874,20 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option { out }; match tag { - 273 | 324 | 513 => strip_off = if tag == 513 { vec![r32(en, 8)] } else { read_vals(count) }, - 279 | 325 | 514 => strip_cnt = if tag == 514 { vec![r32(en, 8)] } else { read_vals(count) }, + 273 | 324 | 513 => { + strip_off = if tag == 513 { + vec![r32(en, 8)] + } else { + read_vals(count) + } + } + 279 | 325 | 514 => { + strip_cnt = if tag == 514 { + vec![r32(en, 8)] + } else { + read_vals(count) + } + } 330 | 34665 | 34853 | 40965 => { if ty == 4 && count == 1 { subs.push(value_off); @@ -739,7 +899,10 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option { } _ => {} } - if (tag == 273 || tag == 324 || tag == 513) && !strip_off.is_empty() && !strip_cnt.is_empty() { + if (tag == 273 || tag == 324 || tag == 513) + && !strip_off.is_empty() + && !strip_cnt.is_empty() + { for (o, c) in strip_off.iter().zip(&strip_cnt) { end = end.max(o + c); } @@ -749,13 +912,23 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option { end = end.max(o + c); } seen += 1; - ifd = if let Some(s) = subs.pop() { s } else { r32(&e, n * 12) }; + ifd = if let Some(s) = subs.pop() { + s + } else { + r32(&e, n * 12) + }; if end > max { return None; } } - let ext = if h.get(8..10) == Some(b"CR") { "cr2" } else { "tif" }; - (end > 16).then(|| ()).and_then(|_| hit(end, ext, Confidence::Medium)) + let ext = if h.get(8..10) == Some(b"CR") { + "cr2" + } else { + "tif" + }; + (end > 16) + .then_some(()) + .and_then(|_| hit(end, ext, Confidence::Medium)) } fn ico(src: &dyn Source, off: u64, max: u64) -> Option { @@ -795,7 +968,17 @@ fn regf(src: &dyn Source, off: u64, max: u64) -> Option { } let ok = x == u32le(&h, 0x1FC) as u32; let len = 4096 + u32le(&h, 0x28); - (len > 4096 && len <= max).then(|| ()).and_then(|_| hit(len, "dat", if ok { Confidence::High } else { Confidence::Medium })) + (len > 4096 && len <= max).then_some(()).and_then(|_| { + hit( + len, + "dat", + if ok { + Confidence::High + } else { + Confidence::Medium + }, + ) + }) } fn evtx(src: &dyn Source, off: u64) -> Option { @@ -817,8 +1000,14 @@ fn pst(src: &dyn Source, off: u64, max: u64) -> Option { return None; } let ver = u16le(&h, 10); - let len = if ver >= 23 { u64le(&h, 0xB8) } else { u32le(&h, 0xA8) }; - (len > 0x200 && len <= max).then(|| ()).and_then(|_| hit(len, "pst", Confidence::High)) + let len = if ver >= 23 { + u64le(&h, 0xB8) + } else { + u32le(&h, 0xA8) + }; + (len > 0x200 && len <= max) + .then_some(()) + .and_then(|_| hit(len, "pst", Confidence::High)) } fn rtf(src: &dyn Source, off: u64, max: u64) -> Option { @@ -863,7 +1052,11 @@ fn pcap(src: &dyn Source, off: u64, max: u64) -> Option { let le = h[0] == 0xD4; let r32 = |b: &[u8], o: usize| -> u64 { let s: [u8; 4] = b[o..o + 4].try_into().unwrap(); - if le { u32::from_le_bytes(s) as u64 } else { u32::from_be_bytes(s) as u64 } + if le { + u32::from_le_bytes(s) as u64 + } else { + u32::from_be_bytes(s) as u64 + } }; let snaplen = r32(&h, 16); if snaplen == 0 || snaplen > 1 << 20 { @@ -896,7 +1089,9 @@ fn pcap(src: &dyn Source, off: u64, max: u64) -> Option { return None; } } - (n > 0).then(|| ()).and_then(|_| hit(pos - off, "pcap", Confidence::Medium)) + (n > 0) + .then_some(()) + .and_then(|_| hit(pos - off, "pcap", Confidence::Medium)) } fn pcapng(src: &dyn Source, off: u64, max: u64) -> Option { @@ -908,7 +1103,11 @@ fn pcapng(src: &dyn Source, off: u64, max: u64) -> Option { }; let r32 = |b: &[u8], o: usize| -> u64 { let s: [u8; 4] = b[o..o + 4].try_into().unwrap(); - if le { u32::from_le_bytes(s) as u64 } else { u32::from_be_bytes(s) as u64 } + if le { + u32::from_le_bytes(s) as u64 + } else { + u32::from_be_bytes(s) as u64 + } }; let mut pos = off; let mut blocks = 0; @@ -931,5 +1130,7 @@ fn pcapng(src: &dyn Source, off: u64, max: u64) -> Option { return None; } } - (blocks >= 2).then(|| ()).and_then(|_| hit(pos - off, "pcapng", Confidence::Medium)) + (blocks >= 2) + .then_some(()) + .and_then(|_| hit(pos - off, "pcapng", Confidence::Medium)) } diff --git a/crates/guara-core/src/fs/apfs.rs b/crates/guara-core/src/fs/apfs.rs index 88dc13e..930963c 100644 --- a/crates/guara-core/src/fs/apfs.rs +++ b/crates/guara-core/src/fs/apfs.rs @@ -9,13 +9,19 @@ use crate::types::{Confidence, DataLoc, Extent, Origin}; use std::collections::{BTreeMap, HashMap, HashSet}; fn le16(b: &[u8], o: usize) -> u16 { - b.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).unwrap_or(0) + b.get(o..o + 2) + .map(|s| u16::from_le_bytes([s[0], s[1]])) + .unwrap_or(0) } fn le32(b: &[u8], o: usize) -> u32 { - b.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 4) + .map(|s| u32::from_le_bytes(s.try_into().unwrap())) + .unwrap_or(0) } fn le64(b: &[u8], o: usize) -> u64 { - b.get(o..o + 8).map(|s| u64::from_le_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 8) + .map(|s| u64::from_le_bytes(s.try_into().unwrap())) + .unwrap_or(0) } const OBJ_TYPE_MASK: u32 = 0xFFFF; @@ -99,22 +105,42 @@ impl<'a> Apfs<'a> { } let ks = key_base + koff; let k = b.get(ks..ks + klen)?.to_vec(); - let v = if voff == 0xFFFF { vec![] } else { b.get(val_end.checked_sub(voff)?..)?.get(..vlen)?.to_vec() }; + let v = if voff == 0xFFFF { + vec![] + } else { + b.get(val_end.checked_sub(voff)?..)?.get(..vlen)?.to_vec() + }; entries.push((k, v)); } - Some(BNode { flags, level, entries, oid: le64(b, 8), xid: le64(b, 16) }) + Some(BNode { + flags, + level, + entries, + oid: le64(b, 8), + xid: le64(b, 16), + }) } /// Todas as entradas de uma árvore física (mapa de objetos): filhos são endereços físicos. - fn walk_physical(&self, root: u64, fixed_k: usize, fixed_v: usize, f: &mut dyn FnMut(&[u8], &[u8])) { + fn walk_physical( + &self, + root: u64, + fixed_k: usize, + fixed_v: usize, + f: &mut dyn FnMut(&[u8], &[u8]), + ) { let mut stack = vec![(root, 0u32)]; let mut seen = HashSet::new(); while let Some((p, depth)) = stack.pop() { if depth > 8 || !seen.insert(p) { continue; } - let Some(b) = self.checked_block(p) else { continue }; - let Some(n) = self.parse_node(&b, fixed_k, fixed_v) else { continue }; + let Some(b) = self.checked_block(p) else { + continue; + }; + let Some(n) = self.parse_node(&b, fixed_k, fixed_v) else { + continue; + }; if n.level == 0 { for (k, v) in &n.entries { f(k, v); @@ -130,10 +156,14 @@ impl<'a> Apfs<'a> { /// Mapa de objetos: oid -> [(xid, paddr)] ordenado por xid. fn omap(&self, omap_paddr: u64) -> HashMap> { let mut m: HashMap> = HashMap::new(); - let Some(ob) = self.checked_block(omap_paddr) else { return m }; + let Some(ob) = self.checked_block(omap_paddr) else { + return m; + }; let tree = le64(&ob, 48); self.walk_physical(tree, 16, 16, &mut |k, v| { - m.entry(le64(k, 0)).or_default().push((le64(k, 8), le64(v, 8))); + m.entry(le64(k, 0)) + .or_default() + .push((le64(k, 8), le64(v, 8))); }); for v in m.values_mut() { v.sort(); @@ -143,7 +173,11 @@ impl<'a> Apfs<'a> { } fn lookup(m: &HashMap>, oid: u64, max_xid: u64) -> Option { - m.get(&oid)?.iter().rev().find(|(x, _)| *x <= max_xid).map(|(_, p)| *p) + m.get(&oid)? + .iter() + .rev() + .find(|(x, _)| *x <= max_xid) + .map(|(_, p)| *p) } // ---------------------------------------------------------------- registros @@ -188,7 +222,7 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) { mtime: ns(le64(v, 24)), atime: ns(le64(v, 40)), mode: le16(v, 80), - size: le64(v, 84).max(0), + size: le64(v, 84), has: true, ..Default::default() }; @@ -204,7 +238,14 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) { let (xt, xs) = (xf[4 + i * 4], le16(xf, 6 + i * 4) as usize); let data = xf.get(p..p + xs).unwrap_or(&[]); match xt { - 4 => ino.name = Some(String::from_utf8_lossy(data.split(|&c| c == 0).next().unwrap_or(&[])).to_string()), + 4 => { + ino.name = Some( + String::from_utf8_lossy( + data.split(|&c| c == 0).next().unwrap_or(&[]), + ) + .to_string(), + ) + } 8 if data.len() >= 8 => ino.size = le64(data, 0), _ => {} } @@ -216,13 +257,17 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) { R_DIR_REC if k.len() > 12 && v.len() >= 18 => { let nl = (le32(k, 8) & 0x3FF) as usize; if let Some(name) = k.get(12..12 + nl) { - let name = String::from_utf8_lossy(name.split(|&c| c == 0).next().unwrap_or(&[])).to_string(); + let name = String::from_utf8_lossy(name.split(|&c| c == 0).next().unwrap_or(&[])) + .to_string(); files.entry(le64(v, 0)).or_default().names.insert(id, name); } } R_FILE_EXTENT if k.len() >= 16 && v.len() >= 24 => { let len = le64(v, 0) & 0x00FF_FFFF_FFFF_FFFF; - extents.entry(id).or_default().insert(le64(k, 8), (len, le64(v, 8), le64(v, 16))); + extents + .entry(id) + .or_default() + .insert(le64(k, 8), (len, le64(v, 8), le64(v, 16))); } _ => {} } @@ -230,16 +275,28 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) { impl Apfs<'_> { /// Percorre a árvore de arquivos a partir de uma raiz virtual. - fn walk_fstree(&self, root_oid: u64, omap: &HashMap>, xid: u64, f: &mut dyn FnMut(&[u8], &[u8], u64)) { - let Some(rp) = lookup(omap, root_oid, xid) else { return }; + fn walk_fstree( + &self, + root_oid: u64, + omap: &HashMap>, + xid: u64, + f: &mut dyn FnMut(&[u8], &[u8], u64), + ) { + let Some(rp) = lookup(omap, root_oid, xid) else { + return; + }; let mut stack = vec![(rp, 0u32)]; let mut seen = HashSet::new(); while let Some((p, depth)) = stack.pop() { if depth > 10 || !seen.insert(p) { continue; } - let Some(b) = self.checked_block(p) else { continue }; - let Some(n) = self.parse_node(&b, 16, 16) else { continue }; + let Some(b) = self.checked_block(p) else { + continue; + }; + let Some(n) = self.parse_node(&b, 16, 16) else { + continue; + }; if n.level == 0 { for (k, v) in &n.entries { f(k, v, p); @@ -261,7 +318,10 @@ fn path_of(files: &Files, mut id: u64) -> String { if id == 2 || id == 1 { break; } - match files.get(&id).and_then(|f| f.names.iter().next().map(|(p, n)| (*p, n.clone()))) { + match files + .get(&id) + .and_then(|f| f.names.iter().next().map(|(p, n)| (*p, n.clone()))) + { Some((p, n)) => { parts.push(n); id = p; @@ -285,7 +345,11 @@ fn parse_volume(b: &[u8]) -> Option { if &b[32..36] != b"APSB" { return None; } - let name = String::from_utf8_lossy(&b[704..960]).split('\0').next().unwrap_or("").to_string(); + let name = String::from_utf8_lossy(&b[704..960]) + .split('\0') + .next() + .unwrap_or("") + .to_string(); Some(Volume { name, omap_paddr: le64(b, 128), @@ -305,7 +369,12 @@ fn open_container<'a>(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec VolumeScan { let _ = part_len; let mut res = VolumeScan::default(); - let Some((a, nx)) = open_container(ctx.src, base) else { return res }; - let Some(cont_omap_block) = a.checked_block(le64(&nx, 160)) else { return res }; + let Some((a, nx)) = open_container(ctx.src, base) else { + return res; + }; + let Some(cont_omap_block) = a.checked_block(le64(&nx, 160)) else { + return res; + }; let _ = cont_omap_block; let container_omap = a.omap(le64(&nx, 160)); let max_fs = le32(&nx, 180).min(100) as usize; @@ -379,8 +452,12 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: if !seen.insert(p) { continue; } - let Some(b) = a.checked_block(p) else { continue }; - let Some(n) = a.parse_node(&b, 0, 0) else { continue }; + let Some(b) = a.checked_block(p) else { + continue; + }; + let Some(n) = a.parse_node(&b, 0, 0) else { + continue; + }; if n.level == 0 { for (k, val) in &n.entries { let hdr = le64(k, 0); @@ -397,8 +474,12 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: } } for (xid, sblock) in snaps { - let Some(sb) = a.checked_block(sblock) else { continue }; - let Some(sv) = parse_volume(&sb) else { continue }; + let Some(sb) = a.checked_block(sblock) else { + continue; + }; + let Some(sv) = parse_volume(&sb) else { + continue; + }; a.walk_fstree(sv.root_oid, &omap, xid, &mut |k, val, _| { add_record(&mut stale, &mut stale_ext, k, val); }); @@ -417,7 +498,10 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: } let Some(b) = a.block(p) else { continue }; let ty = le32(&b, 24) & OBJ_TYPE_MASK; - if (ty != T_BTREE_NODE && ty != T_BTREE_ROOT) || le32(&b, 28) != SUBTYPE_FSTREE || le16(&b, 34) != 0 { + if (ty != T_BTREE_NODE && ty != T_BTREE_ROOT) + || le32(&b, 28) != SUBTYPE_FSTREE + || le16(&b, 34) != 0 + { continue; } if live_nodes.contains(&p) || fletcher64(&b[8..]) != le64(&b, 0) { @@ -436,7 +520,7 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: ids.sort(); let mut seen_first: HashSet = HashSet::new(); for id in ids { - if live.get(id).map_or(false, |f| f.inode.has) { + if live.get(id).is_some_and(|f| f.inode.has) { continue; } let rec = &stale[id]; @@ -444,7 +528,9 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: if !ino.has || ino.mode & 0o170000 != 0o100000 || ino.size == 0 { continue; } - let Some(ex) = stale_ext.get(&ino.private_id) else { continue }; + let Some(ex) = stale_ext.get(&ino.private_id) else { + continue; + }; if ex.values().any(|e| e.2 != 0) { continue; // extensão cifrada } @@ -456,17 +542,29 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: None => (format!("arquivo_{id}"), ino.parent), }, }; - let merged: Files = stale.iter().map(|(k, v)| (*k, v.clone())).chain(live.iter().map(|(k, v)| (*k, v.clone()))).collect(); + let merged: Files = stale + .iter() + .map(|(k, v)| (*k, v.clone())) + .chain(live.iter().map(|(k, v)| (*k, v.clone()))) + .collect(); let dir = Some(format!("/{}", path_of(&merged, parent)).replace("//", "/")); let mut extents: Vec = vec![]; let mut pos = 0u64; let mut ok = true; for (&lo, &(len, paddr, _)) in ex { if lo > pos { - extents.push(Extent { offset: 0, len: lo - pos, sparse: true }); + extents.push(Extent { + offset: 0, + len: lo - pos, + sparse: true, + }); } if paddr == 0 { - extents.push(Extent { offset: 0, len, sparse: true }); + extents.push(Extent { + offset: 0, + len, + sparse: true, + }); } else if paddr < a.blocks { extents.push(Extent::new(a.base + paddr * a.bs, len)); } else { @@ -478,11 +576,17 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: continue; } let extents = super::fat::trim_extents(extents, ino.size); - let Some(first) = extents.iter().find(|e| !e.sparse) else { continue }; + let Some(first) = extents.iter().find(|e| !e.sparse) else { + continue; + }; if !seen_first.insert(first.offset) { continue; } - let note = if ino.compressed { "arquivo comprimido pelo macOS (decmpfs): o conteúdo pode estar incompleto" } else { "versão antiga da árvore ou instantâneo do APFS" }; + let note = if ino.compressed { + "arquivo comprimido pelo macOS (decmpfs): o conteúdo pode estar incompleto" + } else { + "versão antiga da árvore ou instantâneo do APFS" + }; let ts = |t: i64| (t > 0).then_some(t); let nf = NewFound { name, @@ -493,7 +597,11 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: modified: ts(ino.mtime), created: ts(ino.ctime), accessed: ts(ino.atime), - base: if ino.compressed { Confidence::Low } else { Confidence::Medium }, + base: if ino.compressed { + Confidence::Low + } else { + Confidence::Medium + }, note: Some(note), }; let f = make_found(ctx, nf); @@ -504,13 +612,19 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: /// Arquivos e pastas atuais do primeiro volume sem cifragem (caminho, tamanho, é pasta). pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> { - let Some((a, nx)) = open_container(src, base) else { return vec![] }; + let Some((a, nx)) = open_container(src, base) else { + return vec![]; + }; let cont = a.omap(le64(&nx, 160)); let mut out = vec![]; for i in 0..le32(&nx, 180).min(100) as usize { let oid = le64(&nx, 184 + i * 8); - let Some(p) = lookup(&cont, oid, u64::MAX) else { continue }; - let Some(vb) = a.checked_block(p) else { continue }; + let Some(p) = lookup(&cont, oid, u64::MAX) else { + continue; + }; + let Some(vb) = a.checked_block(p) else { + continue; + }; let Some(v) = parse_volume(&vb) else { continue }; if v.encrypted { continue; @@ -518,12 +632,18 @@ pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> { let omap = a.omap(v.omap_paddr); let mut files: Files = HashMap::new(); let mut ext: Extents = HashMap::new(); - a.walk_fstree(v.root_oid, &omap, u64::MAX, &mut |k, val, _| add_record(&mut files, &mut ext, k, val)); + a.walk_fstree(v.root_oid, &omap, u64::MAX, &mut |k, val, _| { + add_record(&mut files, &mut ext, k, val) + }); for (&id, f) in &files { if !f.inode.has || id <= 2 { continue; } - out.push((path_of(&files, id), f.inode.size, f.inode.mode & 0o170000 == 0o040000)); + out.push(( + path_of(&files, id), + f.inode.size, + f.inode.mode & 0o170000 == 0o040000, + )); } } out.sort(); diff --git a/crates/guara-core/src/fs/btrfs.rs b/crates/guara-core/src/fs/btrfs.rs index dcc1d2b..b0cc1f5 100644 --- a/crates/guara-core/src/fs/btrfs.rs +++ b/crates/guara-core/src/fs/btrfs.rs @@ -22,13 +22,19 @@ const METADATA_ITEM: u8 = 169; const CHUNK_ITEM: u8 = 228; fn le16(b: &[u8], o: usize) -> u16 { - b.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).unwrap_or(0) + b.get(o..o + 2) + .map(|s| u16::from_le_bytes([s[0], s[1]])) + .unwrap_or(0) } fn le32(b: &[u8], o: usize) -> u32 { - b.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 4) + .map(|s| u32::from_le_bytes(s.try_into().unwrap())) + .unwrap_or(0) } fn le64(b: &[u8], o: usize) -> u64 { - b.get(o..o + 8).map(|s| u64::from_le_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 8) + .map(|s| u64::from_le_bytes(s.try_into().unwrap())) + .unwrap_or(0) } pub fn crc32c(data: &[u8]) -> u32 { @@ -38,7 +44,11 @@ pub fn crc32c(data: &[u8]) -> u32 { for i in 0..256u32 { let mut c = i; for _ in 0..8 { - c = if c & 1 != 0 { (c >> 1) ^ 0x82F6_3B78 } else { c >> 1 }; + c = if c & 1 != 0 { + (c >> 1) ^ 0x82F6_3B78 + } else { + c >> 1 + }; } t[i as usize] = c; } @@ -59,7 +69,11 @@ pub struct Key { } fn key_at(b: &[u8], o: usize) -> Key { - Key { objectid: le64(b, o), ty: b.get(o + 8).copied().unwrap_or(0), offset: le64(b, o + 9) } + Key { + objectid: le64(b, o), + ty: b.get(o + 8).copied().unwrap_or(0), + offset: le64(b, o + 9), + } } #[derive(Clone, Debug)] @@ -108,7 +122,10 @@ impl<'a> Fs<'a> { } let nodesize = le32(&sb, 148) as u64; let sectorsize = le32(&sb, 144) as u64; - if !nodesize.is_power_of_two() || !(4096..=65536).contains(&nodesize) || !sectorsize.is_power_of_two() { + if !nodesize.is_power_of_two() + || !(4096..=65536).contains(&nodesize) + || !sectorsize.is_power_of_two() + { return None; } let mut fsid = [0u8; 16]; @@ -156,9 +173,17 @@ impl<'a> Fs<'a> { } fn phys(&self, logical: u64) -> Option { - let i = self.chunks.partition_point(|c| c.logical + c.len <= logical); + let i = self + .chunks + .partition_point(|c| c.logical + c.len <= logical); let c = self.chunks.get(i)?; - (c.logical <= logical).then(|| c.phys.first().map(|p| self.base + p + (logical - c.logical))).flatten() + (c.logical <= logical) + .then(|| { + c.phys + .first() + .map(|p| self.base + p + (logical - c.logical)) + }) + .flatten() } fn read_node(&self, logical: u64) -> Option { @@ -172,7 +197,7 @@ impl<'a> Fs<'a> { return None; } let bytenr = le64(raw, 48); - if expect.map_or(false, |e| e != bytenr) { + if expect.is_some_and(|e| e != bytenr) { return None; } if crc32c(&raw[32..]) != le32(raw, 0) { @@ -208,7 +233,12 @@ impl<'a> Fs<'a> { } NodeKind::Internal(ptrs) }; - Some(Node { bytenr, generation: le64(raw, 80), owner: le64(raw, 88), kind }) + Some(Node { + bytenr, + generation: le64(raw, 80), + owner: le64(raw, 88), + kind, + }) } /// Percorre uma árvore em ordem, chamando `f` para cada item de folha. @@ -222,7 +252,14 @@ impl<'a> Fs<'a> { self.walk_inner(root, f, &mut seen, nodes, 0); } - fn walk_inner(&self, at: u64, f: &mut dyn FnMut(&Key, &[u8]), seen: &mut HashSet, nodes: &mut dyn FnMut(&Node), depth: u32) { + fn walk_inner( + &self, + at: u64, + f: &mut dyn FnMut(&Key, &[u8]), + seen: &mut HashSet, + nodes: &mut dyn FnMut(&Node), + depth: u32, + ) { if depth > 10 || !seen.insert(at) { return; } @@ -254,7 +291,15 @@ fn parse_chunk(d: &[u8], logical: u64) -> Option<(Chunk, usize)> { return None; } let phys = (0..nstripes).map(|i| le64(d, 48 + i * 32 + 8)).collect(); - Some((Chunk { logical, len, ty, phys }, 48 + nstripes * 32)) + Some(( + Chunk { + logical, + len, + ty, + phys, + }, + 48 + nstripes * 32, + )) } // ---------------------------------------------------------------- inodes @@ -293,7 +338,11 @@ fn add_item(files: &mut Files, owner: u64, gen: u64, k: &Key, d: &[u8]) { INODE_REF if d.len() >= 10 => { let nlen = le16(d, 8) as usize; if let Some(n) = d.get(10..10 + nlen) { - files.entry((owner, k.objectid)).or_default().refs.insert(k.offset, String::from_utf8_lossy(n).to_string()); + files + .entry((owner, k.objectid)) + .or_default() + .refs + .insert(k.offset, String::from_utf8_lossy(n).to_string()); } } EXTENT_DATA if d.len() >= 21 => { @@ -319,7 +368,11 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { let Some(fs) = Fs::open(ctx.src, base) else { return res; }; - if fs.chunks.iter().any(|c| c.ty & 0x1 << 3 != 0 || c.ty & 0x10 != 0 || c.ty & 0x100 != 0 || c.ty & 0x200 != 0) { + if fs + .chunks + .iter() + .any(|c| c.ty & 0x1 << 3 != 0 || c.ty & 0x10 != 0 || c.ty & 0x100 != 0 || c.ty & 0x200 != 0) + { res.warnings.push("Btrfs em RAID 0/10/5/6: a leitura de metadados ainda não suporta esses perfis; usando só assinaturas.".into()); return res; } @@ -404,11 +457,18 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { let mut stale: Files = HashMap::new(); let ns = fs.nodesize as usize; let step = fs.sectorsize as usize; - let md_chunks: Vec = fs.chunks.iter().filter(|c| c.ty & 0x4 != 0).cloned().collect(); + let md_chunks: Vec = fs + .chunks + .iter() + .filter(|c| c.ty & 0x4 != 0) + .cloned() + .collect(); let total_md: u64 = md_chunks.iter().map(|c| c.len).sum(); let mut done_md = 0u64; for c in &md_chunks { - let Some(p0) = c.phys.first().copied() else { continue }; + let Some(p0) = c.phys.first().copied() else { + continue; + }; let mut off = 0u64; let block = 8usize << 20; while off < c.len { @@ -416,12 +476,17 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { return res; } let n = ((c.len - off) as usize).min(block + ns); - let Ok(buf) = read_vec(ctx.src, fs.base + p0 + off, n) else { break }; + let Ok(buf) = read_vec(ctx.src, fs.base + p0 + off, n) else { + break; + }; let mut i = 0usize; while i + ns <= buf.len() && (off + i as u64) < c.len { let logical = c.logical + off + i as u64; // cabeçalho barato antes do CRC - if buf[i + 32..i + 48] == fs.fsid && le64(&buf[i..], 48) == logical && buf[i + 100] == 0 { + if buf[i + 32..i + 48] == fs.fsid + && le64(&buf[i..], 48) == logical + && buf[i + 100] == 0 + { if let Some(node) = fs.parse_node(&buf[i..i + ns], Some(logical)) { if is_fs_tree(node.owner) && !live_nodes.contains(&logical) { if let NodeKind::Leaf(items) = &node.kind { @@ -473,18 +538,32 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { if !e.has_inode || e.mode & 0o170000 != 0o100000 || e.size == 0 || e.extents.is_empty() { continue; } - let Some((name, parent)) = e.refs.iter().next().map(|(p, n)| (n.clone(), *p)) else { continue }; + let Some((name, parent)) = e.refs.iter().next().map(|(p, n)| (n.clone(), *p)) else { + continue; + }; let dir = { let p = path_of(owner, parent); - Some(if p.is_empty() { String::new() } else { format!("/{p}") }) + Some(if p.is_empty() { + String::new() + } else { + format!("/{p}") + }) + }; + let Some((data, any_alloc, all_ok)) = build_data(&fs, e, &allocated) else { + continue; }; - let Some((data, any_alloc, all_ok)) = build_data(&fs, e, &allocated) else { continue }; let (conf, note) = if !all_ok { - (Confidence::Low, "faltam trechos do arquivo na versão antiga da árvore".to_string()) + ( + Confidence::Low, + "faltam trechos do arquivo na versão antiga da árvore".to_string(), + ) } else if any_alloc { (Confidence::Medium, "versão antiga do Btrfs; alguns blocos ainda constam como alocados (podem ser de outro arquivo)".to_string()) } else { - (Confidence::High, "versão antiga da árvore do Btrfs; blocos de dados livres".to_string()) + ( + Confidence::High, + "versão antiga da árvore do Btrfs; blocos de dados livres".to_string(), + ) }; let ts = |t: i64| (t > 0).then_some(t); let nf = NewFound { @@ -526,7 +605,10 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc, } if foff > pos { sparse_marks.push(parts.len()); - parts.push(Part::Run { offset: 0, len: foff - pos }); + parts.push(Part::Run { + offset: 0, + len: foff - pos, + }); plain_only = false; pos = foff; } @@ -541,7 +623,9 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc, c => decode(c, raw, ram)?, }; let len = (bytes.len() as u64).min(e.size - foff); - parts.push(Part::Bytes { bytes: bytes[..len as usize].to_vec() }); + parts.push(Part::Bytes { + bytes: bytes[..len as usize].to_vec(), + }); plain_only = false; pos = foff + len; continue; @@ -554,7 +638,10 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc, let num = num.min(e.size.saturating_sub(foff)); if disk == 0 || ty == 2 { sparse_marks.push(parts.len()); - parts.push(Part::Run { offset: 0, len: num }); + parts.push(Part::Run { + offset: 0, + len: num, + }); plain_only = false; } else { let Some(phys) = fs.phys(disk) else { @@ -563,22 +650,32 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc, }; let overlaps = { let i = allocated.partition_point(|x| x.end() <= disk); - allocated.get(i).map_or(false, |x| x.offset < disk + disk_len) + allocated.get(i).is_some_and(|x| x.offset < disk + disk_len) }; any_alloc |= overlaps; if comp == 0 { - parts.push(Part::Run { offset: phys + offset, len: num }); + parts.push(Part::Run { + offset: phys + offset, + len: num, + }); } else { let codec = match comp { 1 => PackedCodec::Zlib, 2 => PackedCodec::Zstd, - 3 | 4 | 5 | 6 => PackedCodec::Lzo, + 3..=6 => PackedCodec::Lzo, _ => { all_ok = false; break; } }; - parts.push(Part::Packed(Packed { offset: phys, disk_len, ram_len: le64(d, 8), skip: offset, len: num, codec })); + parts.push(Part::Packed(Packed { + offset: phys, + disk_len, + ram_len: le64(d, 8), + skip: offset, + len: num, + codec, + })); plain_only = false; } } @@ -588,7 +685,16 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc, all_ok = false; } if plain_only { - let ex: Vec = parts.iter().filter_map(|p| if let Part::Run { offset, len } = p { Some(Extent::new(*offset, *len)) } else { None }).collect(); + let ex: Vec = parts + .iter() + .filter_map(|p| { + if let Part::Run { offset, len } = p { + Some(Extent::new(*offset, *len)) + } else { + None + } + }) + .collect(); return Some((DataLoc::Extents { extents: ex }, any_alloc, all_ok)); } // Buracos viram trechos esparsos: Part::Run com offset 0 não serve; usa Bytes de zeros pequenos. @@ -610,7 +716,9 @@ fn decode(codec: u8, raw: &[u8], ram: usize) -> Option> { 1 => { use std::io::Read; let mut v = Vec::with_capacity(ram); - flate2::read::ZlibDecoder::new(raw).read_to_end(&mut v).ok()?; + flate2::read::ZlibDecoder::new(raw) + .read_to_end(&mut v) + .ok()?; Some(v) } 2 => { @@ -695,7 +803,11 @@ pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option> { } let h = byte(ip)?; ip += 1; - let (dist, len) = if state == 4 { ((t >> 2) + (h << 2) + 2049, 3) } else { ((t >> 2) + (h << 2) + 1, 2) }; + let (dist, len) = if state == 4 { + ((t >> 2) + (h << 2) + 2049, 3) + } else { + ((t >> 2) + (h << 2) + 1, 2) + }; copy_match(&mut out, dist, len)?; let next = t & 3; lits(&mut out, &mut ip, next)?; @@ -708,7 +820,11 @@ pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option> { lits(&mut out, &mut ip, next)?; state = next; } else if t >= 32 { - let len = if t & 31 == 0 { ext(&mut ip, 31)? } else { t & 31 } + 2; + let len = if t & 31 == 0 { + ext(&mut ip, 31)? + } else { + t & 31 + } + 2; let b = (byte(ip)?) | (byte(ip + 1)? << 8); ip += 2; copy_match(&mut out, 1 + (b >> 2), len)?; @@ -745,7 +861,9 @@ fn copy_match(out: &mut Vec, dist: usize, len: usize) -> Option<()> { /// Arquivos e pastas atuais com caminho completo e tamanho (usa só as árvores vivas). pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> { - let Some(fs) = Fs::open(src, base) else { return vec![] }; + let Some(fs) = Fs::open(src, base) else { + return vec![]; + }; let mut roots: Vec<(u64, u64)> = vec![]; fs.walk(fs.root, &mut |k, d| { if k.ty == ROOT_ITEM && d.len() >= 239 && is_fs_tree(k.objectid) { @@ -817,6 +935,10 @@ pub fn locate_trees(src: &dyn Source, base: u64) -> Option { root_tree_logical: fs.root, fs_tree_logical: fs_t, extent_tree_logical: ext_t, - chunks: fs.chunks.iter().filter_map(|c| c.phys.first().map(|p| (c.logical, c.len, fs.base + p))).collect(), + chunks: fs + .chunks + .iter() + .filter_map(|c| c.phys.first().map(|p| (c.logical, c.len, fs.base + p))) + .collect(), }) } diff --git a/crates/guara-core/src/fs/ext.rs b/crates/guara-core/src/fs/ext.rs index 458dfc8..8e75dfa 100644 --- a/crates/guara-core/src/fs/ext.rs +++ b/crates/guara-core/src/fs/ext.rs @@ -18,13 +18,19 @@ const EXT_MAGIC: u16 = 0xF30A; const JBD2_MAGIC: u32 = 0xC03B_3998; fn le16(b: &[u8], o: usize) -> u16 { - b.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).unwrap_or(0) + b.get(o..o + 2) + .map(|s| u16::from_le_bytes([s[0], s[1]])) + .unwrap_or(0) } fn le32(b: &[u8], o: usize) -> u32 { - b.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 4) + .map(|s| u32::from_le_bytes(s.try_into().unwrap())) + .unwrap_or(0) } fn be32(b: &[u8], o: usize) -> u32 { - b.get(o..o + 4).map(|s| u32::from_be_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 4) + .map(|s| u32::from_be_bytes(s.try_into().unwrap())) + .unwrap_or(0) } #[derive(Clone, Copy, Default)] @@ -102,7 +108,9 @@ type ExtentList = Vec<(u64, u64, u64, bool)>; impl<'a> Ext<'a> { fn open(src: &'a dyn Source, base: u64) -> Option { - let sb = read_vec(src, base + 1024, 1024).ok().filter(|s| s.len() == 1024)?; + let sb = read_vec(src, base + 1024, 1024) + .ok() + .filter(|s| s.len() == 1024)?; if le16(&sb, 56) != 0xEF53 { return None; } @@ -125,7 +133,11 @@ impl<'a> Ext<'a> { if !(128..=4096).contains(&inode_size) { return None; } - let desc_size = if is64 { (le16(&sb, 254) as u64).max(64) } else { 32 }; + let desc_size = if is64 { + (le16(&sb, 254) as u64).max(64) + } else { + 32 + }; let ngroups = blocks.saturating_sub(first_data).div_ceil(bpg); let gdt_block = first_data + 1; let gdt = read_vec(src, base + gdt_block * bs, (ngroups * desc_size) as usize).ok()?; @@ -190,7 +202,13 @@ impl<'a> Ext<'a> { } /// Extents de uma árvore ext4 (raiz em `node`, 60 bytes ou um bloco). - fn walk_extent_node(&self, node: &[u8], depth_limit: u32, out: &mut ExtentList, strict: bool) -> bool { + fn walk_extent_node( + &self, + node: &[u8], + depth_limit: u32, + out: &mut ExtentList, + strict: bool, + ) -> bool { if node.len() < 12 || le16(node, 0) != EXT_MAGIC { return false; } @@ -219,7 +237,9 @@ impl<'a> Ext<'a> { out.push((lblk, start, len, uninit)); } else { let child = ((le16(e, 8) as u64) << 32) | le32(e, 4) as u64; - let Some(b) = self.read_block(child) else { return false }; + let Some(b) = self.read_block(child) else { + return false; + }; if !self.walk_extent_node(&b, depth - 1, out, true) { return false; } @@ -249,14 +269,12 @@ impl<'a> Ext<'a> { v.truncate(max_blocks as usize); } else { ind(ptr(12), &mut v); - if (v.len() as u64) < max_blocks { - if ptr(13) != 0 { - if let Some(d) = self.read_block(ptr(13)) { - for i in 0..ppb as usize { - ind(le32(&d, i * 4) as u64, &mut v); - if v.len() as u64 >= max_blocks { - break; - } + if (v.len() as u64) < max_blocks && ptr(13) != 0 { + if let Some(d) = self.read_block(ptr(13)) { + for i in 0..ppb as usize { + ind(le32(&d, i * 4) as u64, &mut v); + if v.len() as u64 >= max_blocks { + break; } } } @@ -311,14 +329,22 @@ impl<'a> Ext<'a> { } if l > cur_l { let hole = ((l - cur_l) * self.bs).min(left); - out.push(Extent { offset: 0, len: hole, sparse: true }); + out.push(Extent { + offset: 0, + len: hole, + sparse: true, + }); left -= hole; if left == 0 { break; } } let take = (n * self.bs).min(left); - out.push(Extent { offset: self.base + p * self.bs, len: take, sparse: uninit }); + out.push(Extent { + offset: self.base + p * self.bs, + len: take, + sparse: uninit, + }); left -= take; cur_l = l + n; } @@ -340,9 +366,13 @@ impl<'a> Ext<'a> { if gr.flags & 0x2 != 0 { used_bits.fill(false); free.push(Extent::new(self.base + gstart * self.bs, gblocks * self.bs)); - } else if let Some(bm) = read_vec(self.src, self.base + gr.block_bitmap * self.bs, gblocks.div_ceil(8) as usize) - .ok() - .filter(|b| b.len() as u64 == gblocks.div_ceil(8)) + } else if let Some(bm) = read_vec( + self.src, + self.base + gr.block_bitmap * self.bs, + gblocks.div_ceil(8) as usize, + ) + .ok() + .filter(|b| b.len() as u64 == gblocks.div_ceil(8)) { let mut run: Option = None; for i in 0..gblocks { @@ -351,14 +381,20 @@ impl<'a> Ext<'a> { match (used, run) { (false, None) => run = Some(i), (true, Some(s)) => { - free.push(Extent::new(self.base + (gstart + s) * self.bs, (i - s) * self.bs)); + free.push(Extent::new( + self.base + (gstart + s) * self.bs, + (i - s) * self.bs, + )); run = None; } _ => {} } } if let Some(s) = run { - free.push(Extent::new(self.base + (gstart + s) * self.bs, (gblocks - s) * self.bs)); + free.push(Extent::new( + self.base + (gstart + s) * self.bs, + (gblocks - s) * self.bs, + )); } } allocated_all.extend(used_bits); @@ -395,7 +431,11 @@ fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec) { let real = (8 + nlen).div_ceil(4) * 4; if ino != 0 && nlen > 0 && ino <= max_ino { let name = String::from_utf8_lossy(&b[off + 8..off + 8 + nlen]).to_string(); - out.push(DirEnt { ino, name, live: true }); + out.push(DirEnt { + ino, + name, + live: true, + }); } // Folga: entradas apagadas ficam coladas depois do fim "real" desta. let mut s = off + if ino != 0 || nlen > 0 { real } else { 8 }; @@ -413,7 +453,9 @@ fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec) { && drec >= 8 + dn && drec % 4 == 0 && s + drec <= end - && b[s + 8..s + 8 + dn].iter().all(|&c| c >= 0x20 && c != b'/' && c != 0x7f); + && b[s + 8..s + 8 + dn] + .iter() + .all(|&c| c >= 0x20 && c != b'/' && c != 0x7f); if !ok { s += 4; continue; @@ -437,16 +479,22 @@ struct Names { } fn walk_dirs(ext: &Ext, ctx: &Ctx) -> Names { - let mut names = Names { live: HashMap::new(), dead: HashMap::new() }; + let mut names = Names { + live: HashMap::new(), + dead: HashMap::new(), + }; let mut queue: Vec<(u64, String)> = vec![(2, String::new())]; let mut seen: HashSet = HashSet::new(); let mut visited = 0usize; while let Some((dino, path)) = queue.pop() { - if !seen.insert(dino) || (visited % 256 == 255 && (ctx.cancelled)()) || visited > 2_000_000 { + if !seen.insert(dino) || (visited % 256 == 255 && (ctx.cancelled)()) || visited > 2_000_000 + { continue; } visited += 1; - let Some(ino) = ext.read_inode(dino).filter(|i| i.is_dir()) else { continue }; + let Some(ino) = ext.read_inode(dino).filter(|i| i.is_dir()) else { + continue; + }; let mut ents = vec![]; for e in ext.file_extents(&ino, ino.size.min(64 << 20)) { if e.sparse { @@ -467,10 +515,16 @@ fn walk_dirs(ext: &Ext, ctx: &Ctx) -> Names { continue; } if en.live { - names.live.entry(en.ino).or_insert_with(|| (path.clone(), en.name.clone())); + names + .live + .entry(en.ino) + .or_insert_with(|| (path.clone(), en.name.clone())); queue.push((en.ino, format!("{}/{}", path, en.name))); } else { - names.dead.entry(en.ino).or_insert_with(|| (path.clone(), en.name)); + names + .dead + .entry(en.ino) + .or_insert_with(|| (path.clone(), en.name)); } } } @@ -485,7 +539,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap>> { if ext.journal_inum == 0 { return out; } - let Some(jino) = ext.read_inode(ext.journal_inum as u64) else { return out }; + let Some(jino) = ext.read_inode(ext.journal_inum as u64) else { + return out; + }; let jext = ext.file_extents(&jino, jino.size.min(1 << 30)); // Blocos físicos do diário em ordem lógica. let mut jblocks: Vec = vec![]; @@ -500,7 +556,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap>> { if jblocks.len() < 4 { return out; } - let Some(jsb) = ext.read_block(jblocks[0]) else { return out }; + let Some(jsb) = ext.read_block(jblocks[0]) else { + return out; + }; if be32(&jsb, 0) != JBD2_MAGIC { return out; } @@ -512,13 +570,21 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap>> { let is64 = incompat & 0x2 != 0; let csum_v3 = incompat & 0x10 != 0; let csum_v2 = incompat & 0x8 != 0; - let tag_bytes = if csum_v3 { 16 } else { 8 + if is64 { 4 } else { 0 } }; + let tag_bytes = if csum_v3 { + 16 + } else { + 8 + if is64 { 4 } else { 0 } + }; let have_csum = csum_v2 || csum_v3; // Tabela de inodes: faixa de blocos de cada grupo. let itable_blocks = (ext.ipg * ext.inode_size).div_ceil(ext.bs); let mut tables: Vec<(u64, u64, u64)> = vec![]; // (início, fim, primeiro inode) for (g, gr) in ext.groups.iter().enumerate() { - tables.push((gr.inode_table, gr.inode_table + itable_blocks, g as u64 * ext.ipg + 1)); + tables.push(( + gr.inode_table, + gr.inode_table + itable_blocks, + g as u64 * ext.ipg + 1, + )); } let n = jblocks.len(); let first = (be32(&jsb, 20) as usize).clamp(1, n); @@ -540,7 +606,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap>> { } seqs.sort_by_key(|s| s.0); for (_seq, didx) in seqs { - let Some(d) = ext.read_block(jblocks[didx]) else { continue }; + let Some(d) = ext.read_block(jblocks[didx]) else { + continue; + }; let limit = d.len() - if have_csum { 4 } else { 0 }; let mut off = 12usize; let mut data_idx = didx + 1; @@ -552,14 +620,21 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap>> { let (flags, hi) = if csum_v3 { (be32(&d, off + 4), be32(&d, off + 8)) } else { - (u16::from_be_bytes([d[off + 6], d[off + 7]]) as u32, if is64 { be32(&d, off + 8) } else { 0 }) + ( + u16::from_be_bytes([d[off + 6], d[off + 7]]) as u32, + if is64 { be32(&d, off + 8) } else { 0 }, + ) }; blk |= (hi as u64) << 32; off += tag_bytes; if flags & 2 == 0 { off += 16; } - let di = if data_idx >= n { first + (data_idx - n) } else { data_idx }; + let di = if data_idx >= n { + first + (data_idx - n) + } else { + data_idx + }; data_idx += 1; if flags & 4 == 0 { if let Some(t) = tables.iter().position(|t| blk >= t.0 && blk < t.1) { @@ -570,9 +645,12 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap>> { let per_block = (ext.bs / ext.inode_size) as usize; let first_ino = tables[t].2 + (blk - tables[t].0) * per_block as u64; for k in 0..per_block { - let rec = &data[k * ext.inode_size as usize..(k + 1) * ext.inode_size as usize]; + let rec = &data + [k * ext.inode_size as usize..(k + 1) * ext.inode_size as usize]; if rec[..128].iter().any(|&x| x != 0) { - out.entry(first_ino + k as u64).or_default().push(rec.to_vec()); + out.entry(first_ino + k as u64) + .or_default() + .push(rec.to_vec()); } } } @@ -612,13 +690,20 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { continue; // INODE_UNINIT } let used = ext.ipg.saturating_sub(gr.itable_unused as u64).min(ext.ipg); - let Some(table) = read_vec(ext.src, ext.base + gr.inode_table * ext.bs, (used * ext.inode_size) as usize).ok() else { + let Some(table) = read_vec( + ext.src, + ext.base + gr.inode_table * ext.bs, + (used * ext.inode_size) as usize, + ) + .ok() else { continue; }; for i in 0..(table.len() as u64 / ext.inode_size) { let ino_no = g as u64 * ext.ipg + i + 1; let raw = &table[(i * ext.inode_size) as usize..((i + 1) * ext.inode_size) as usize]; - let Some(ino) = Inode::parse(raw) else { continue }; + let Some(ino) = Inode::parse(raw) else { + continue; + }; if names.live.contains_key(&ino_no) || ino_no < 11 || ino.mode == 0 { continue; } @@ -645,7 +730,16 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { cand = Some((ino.clone(), "inode")); } if let Some((c, src_kind)) = cand { - if emit_inode(ctx, &ext, &alloc, &names, ino_no, &c, src_kind, &mut seen_extents) { + if emit_inode( + ctx, + &ext, + &alloc, + &names, + ino_no, + &c, + src_kind, + &mut seen_extents, + ) { emitted.insert(ino_no); } } @@ -662,15 +756,31 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { } let cur = ext.read_inode(*ino_no); // Se o inode atual está vivo e é outro arquivo, não mexe. - if cur.as_ref().map(|c| c.dtime == 0 && c.links > 0 && c.mode != 0).unwrap_or(false) { + if cur + .as_ref() + .map(|c| c.dtime == 0 && c.links > 0 && c.mode != 0) + .unwrap_or(false) + { continue; } for raw in snaps.iter().rev() { if let Some(j) = Inode::parse(raw) { - if j.is_reg() && j.size > 0 && j.dtime == 0 && j.links > 0 { - if emit_inode(ctx, &ext, &alloc, &names, *ino_no, &j, "diário", &mut seen_extents) { - break; - } + if j.is_reg() + && j.size > 0 + && j.dtime == 0 + && j.links > 0 + && emit_inode( + ctx, + &ext, + &alloc, + &names, + *ino_no, + &j, + "diário", + &mut seen_extents, + ) + { + break; } } } @@ -716,7 +826,11 @@ fn emit_inode( if extents.is_empty() || extents.iter().all(|e| e.sparse) { return false; } - let first = extents.iter().find(|e| !e.sparse).map(|e| (e.offset, e.len)).unwrap(); + let first = extents + .iter() + .find(|e| !e.sparse) + .map(|e| (e.offset, e.len)) + .unwrap(); if !seen.insert((first.0, first.1)) { return false; } @@ -735,8 +849,14 @@ fn emit_inode( let mut note = Some(format!("recuperado do {src_kind} do ext4")); let mut conf = Confidence::High; if reused > 0 { - conf = if reused * 2 > total_blocks { Confidence::Low } else { Confidence::Medium }; - note = Some(format!("{reused} de {total_blocks} blocos já foram reaproveitados por outros arquivos")); + conf = if reused * 2 > total_blocks { + Confidence::Low + } else { + Confidence::Medium + }; + note = Some(format!( + "{reused} de {total_blocks} blocos já foram reaproveitados por outros arquivos" + )); } if estimated { // Tamanho desconhecido: tenta achar o fim real pelo conteúdo. @@ -754,7 +874,8 @@ fn emit_inode( } if estimated { conf = conf.min(Confidence::Medium); - note = Some("tamanho exato perdido na exclusão; usando todos os blocos do arquivo".into()); + note = + Some("tamanho exato perdido na exclusão; usando todos os blocos do arquivo".into()); } } let (dir, name) = match names.dead.get(&ino_no).or_else(|| names.live.get(&ino_no)) { @@ -789,14 +910,22 @@ fn layout_from_list(ext: &Ext, list: &ExtentList, size: u64) -> Vec { } if l > cur { let hole = ((l - cur) * ext.bs).min(left); - out.push(Extent { offset: 0, len: hole, sparse: true }); + out.push(Extent { + offset: 0, + len: hole, + sparse: true, + }); left -= hole; if left == 0 { break; } } let take = (n * ext.bs).min(left); - out.push(Extent { offset: ext.base + p * ext.bs, len: take, sparse: uninit }); + out.push(Extent { + offset: ext.base + p * ext.bs, + len: take, + sparse: uninit, + }); left -= take; cur = l + n; } @@ -841,7 +970,9 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option { if !seen.insert(d) || total >= 600 { continue; } - let Some(ino) = ext.read_inode(d).filter(|i| i.is_dir()) else { continue }; + let Some(ino) = ext.read_inode(d).filter(|i| i.is_dir()) else { + continue; + }; let mut ents = vec![]; for e in ext.file_extents(&ino, ino.size.min(1 << 20)) { if e.sparse { @@ -863,7 +994,10 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option { if ents.iter().any(|e| e.live && e.name == "." && e.ino == d) { dots_ok += 1; } - for en in ents.iter().filter(|e| e.live && e.name != "." && e.name != "..") { + for en in ents + .iter() + .filter(|e| e.live && e.name != "." && e.name != "..") + { total += 1; if let Some(i) = ext.read_inode(en.ino) { if i.mode != 0 && i.links > 0 { @@ -882,6 +1016,10 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option { return None; } let a = ok as f64 / total as f64; - let b = if dots_total > 0 { dots_ok as f64 / dots_total as f64 } else { 0.0 }; + let b = if dots_total > 0 { + dots_ok as f64 / dots_total as f64 + } else { + 0.0 + }; Some(1.0 + 5.0 * a + 5.0 * b) } diff --git a/crates/guara-core/src/fs/hfsplus.rs b/crates/guara-core/src/fs/hfsplus.rs index 6d11ba9..c95bf3c 100644 --- a/crates/guara-core/src/fs/hfsplus.rs +++ b/crates/guara-core/src/fs/hfsplus.rs @@ -12,13 +12,19 @@ use std::collections::{HashMap, HashSet}; const HFS_EPOCH: i64 = 2_082_844_800; fn be16(b: &[u8], o: usize) -> u16 { - b.get(o..o + 2).map(|s| u16::from_be_bytes([s[0], s[1]])).unwrap_or(0) + b.get(o..o + 2) + .map(|s| u16::from_be_bytes([s[0], s[1]])) + .unwrap_or(0) } fn be32(b: &[u8], o: usize) -> u32 { - b.get(o..o + 4).map(|s| u32::from_be_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 4) + .map(|s| u32::from_be_bytes(s.try_into().unwrap())) + .unwrap_or(0) } fn be64(b: &[u8], o: usize) -> u64 { - b.get(o..o + 8).map(|s| u64::from_be_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 8) + .map(|s| u64::from_be_bytes(s.try_into().unwrap())) + .unwrap_or(0) } #[derive(Clone, Debug, Default)] @@ -29,7 +35,11 @@ struct Fork { } fn parse_fork(b: &[u8]) -> Fork { - let mut f = Fork { size: be64(b, 0), blocks: be32(b, 12), extents: vec![] }; + let mut f = Fork { + size: be64(b, 0), + blocks: be32(b, 12), + extents: vec![], + }; for i in 0..8 { let (s, c) = (be32(b, 16 + i * 8), be32(b, 20 + i * 8)); if c != 0 { @@ -105,9 +115,13 @@ impl<'a> Vol<'a> { have = ex.iter().map(|e| e.1 as u64).sum(); let _ = have; } - merge_extents(ex.into_iter().map(|(s, c)| Extent::new(self.block_off(s as u64), c as u64 * self.bs)).collect::>()) - .into_iter() - .collect() + merge_extents( + ex.into_iter() + .map(|(s, c)| Extent::new(self.block_off(s as u64), c as u64 * self.bs)) + .collect::>(), + ) + .into_iter() + .collect() } /// Extensões além das oito iniciais, procuradas no arquivo de overflow (árvore B+). @@ -119,7 +133,9 @@ impl<'a> Vol<'a> { }; let total_nodes = self.extents_file.size / node_size as u64; for n in 1..total_nodes.min(100_000) { - let Some(node) = self.read_tree_node(&self.extents_file, n as u32, node_size) else { continue }; + let Some(node) = self.read_tree_node(&self.extents_file, n as u32, node_size) else { + continue; + }; if node[8] != 0xFF { continue; } @@ -193,8 +209,13 @@ fn records(node: &[u8]) -> Vec<&[u8]> { } fn utf16be(b: &[u8]) -> String { - let u: Vec = b.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect(); - String::from_utf16_lossy(&u).replace('/', "_").replace(':', "/") + let u: Vec = b + .chunks_exact(2) + .map(|c| u16::from_be_bytes([c[0], c[1]])) + .collect(); + String::from_utf16_lossy(&u) + .replace('/', "_") + .replace(':', "/") } struct CatFile { @@ -213,7 +234,14 @@ fn date(d: u32) -> Option { } /// Decodifica um registro do catálogo (chave + dados). `max_id` descarta lixo. -fn decode_record(r: &[u8], max_id: u32, files: &mut Vec, threads: &mut HashMap, folders: &mut HashMap<(u32, String), u32>, stale: bool) { +fn decode_record( + r: &[u8], + max_id: u32, + files: &mut Vec, + threads: &mut HashMap, + folders: &mut HashMap<(u32, String), u32>, + stale: bool, +) { let klen = be16(r, 0) as usize; if klen < 6 || klen + 2 > r.len() { return; @@ -240,7 +268,10 @@ fn decode_record(r: &[u8], max_id: u32, files: &mut Vec, threads: &mut // thread de pasta: chave = (id da pasta, ""); dados = (pai, nome) let pn = be16(rec, 8) as usize; if pn <= 255 { - threads.entry(parent).or_insert((be32(rec, 4), utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[])))); + threads.entry(parent).or_insert(( + be32(rec, 4), + utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[])), + )); } } 2 if rec.len() >= 248 => { @@ -263,7 +294,10 @@ fn decode_record(r: &[u8], max_id: u32, files: &mut Vec, threads: &mut 4 if rec.len() >= 10 => { let pn = be16(rec, 8) as usize; if pn <= 255 { - threads.entry(parent).or_insert((be32(rec, 4), utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[])))); + threads.entry(parent).or_insert(( + be32(rec, 4), + utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[])), + )); } } _ => {} @@ -271,7 +305,13 @@ fn decode_record(r: &[u8], max_id: u32, files: &mut Vec, threads: &mut } /// Procura registros de arquivo nas sobras de um nó (entre os dados e a tabela de offsets). -fn scan_slack(node: &[u8], max_id: u32, files: &mut Vec, threads: &mut HashMap, folders: &mut HashMap<(u32, String), u32>) { +fn scan_slack( + node: &[u8], + max_id: u32, + files: &mut Vec, + threads: &mut HashMap, + folders: &mut HashMap<(u32, String), u32>, +) { let n = be16(node, 10) as usize; let table = node.len().saturating_sub(2 * (n + 1)); let used_end = (be16(node, table) as usize).max(14); @@ -284,7 +324,7 @@ fn scan_slack(node: &[u8], max_id: u32, files: &mut Vec, threads: &mut if parent >= 1 && parent < max_id.max(16) { let d = (p + 2 + klen + 1) & !1; let rt = be16(node, d); - if matches!(rt, 1 | 2 | 3 | 4) { + if matches!(rt, 1..=4) { let end = (d + 248).min(table); decode_record(&node[p..end], max_id, files, threads, folders, true); p = d + if rt == 2 { 248 } else { 16 }; @@ -299,15 +339,22 @@ fn scan_slack(node: &[u8], max_id: u32, files: &mut Vec, threads: &mut pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { let mut res = VolumeScan::default(); let _ = part_len; - let Some(vbase) = locate(ctx.src, base) else { return res }; - let Some(vol) = Vol::open(ctx.src, vbase) else { return res }; + let Some(vbase) = locate(ctx.src, base) else { + return res; + }; + let Some(vol) = Vol::open(ctx.src, vbase) else { + return res; + }; // Mapa de espaço livre. if let Some(bm) = vol.read_fork_all(&vol.alloc, (vol.total / 8 + 1) as usize) { let mut free = vec![]; let mut run: Option = None; for b in 0..vol.total { - let used = bm.get((b / 8) as usize).map(|x| x >> (7 - (b % 8)) & 1 == 1).unwrap_or(true); + let used = bm + .get((b / 8) as usize) + .map(|x| x >> (7 - (b % 8)) & 1 == 1) + .unwrap_or(true); match (used, run) { (false, None) => run = Some(b), (true, Some(s)) => { @@ -327,7 +374,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { res.warnings.push("HFS+: catálogo ilegível.".into()); return res; }; - let Some(hdr) = vol.read_tree_node(&vol.catalog, 0, ns) else { return res }; + let Some(hdr) = vol.read_tree_node(&vol.catalog, 0, ns) else { + return res; + }; let first_leaf = be32(&hdr, 24); let max_id = vol.next_cnid.max(16); let mut files: Vec = vec![]; @@ -342,7 +391,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { if (ctx.cancelled)() { return res; } - let Some(node) = vol.read_tree_node(&vol.catalog, n, ns) else { break }; + let Some(node) = vol.read_tree_node(&vol.catalog, n, ns) else { + break; + }; if node[8] != 0xFF { break; } @@ -360,7 +411,15 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { // Diário: cópias antigas de nós folha do catálogo. if vol.journal_info != 0 { - scan_journal(&vol, ns, max_id, &mut files, &mut threads, &mut folders, ctx); + scan_journal( + &vol, + ns, + max_id, + &mut files, + &mut threads, + &mut folders, + ctx, + ); } // Caminho de cada pasta a partir dos threads. @@ -385,13 +444,19 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { let free_set = res.free.clone().unwrap_or_default(); let mut seen: HashSet<(u32, u64)> = HashSet::new(); - for f in files.iter().filter(|f| f.stale && !live_ids.contains(&f.id)) { + for f in files + .iter() + .filter(|f| f.stale && !live_ids.contains(&f.id)) + { if f.data.size == 0 || f.data.extents.is_empty() { continue; } let ex = vol.fork_extents(&f.data, f.id, 0); let total_len: u64 = ex.iter().map(|e| e.len).sum(); - if ex.is_empty() || total_len < f.data.size || ex.iter().any(|e| e.end() > vol.base + vol.total * vol.bs) { + if ex.is_empty() + || total_len < f.data.size + || ex.iter().any(|e| e.end() > vol.base + vol.total * vol.bs) + { continue; } if !seen.insert((f.id, ex[0].offset)) { @@ -402,9 +467,15 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { // Blocos já reaproveitados? let reused = extents.iter().filter(|e| !covered_by(&free_set, e)).count(); let (conf, note) = if reused == 0 { - (Confidence::High, "registro apagado do catálogo HFS+; blocos ainda livres".to_string()) + ( + Confidence::High, + "registro apagado do catálogo HFS+; blocos ainda livres".to_string(), + ) } else { - (Confidence::Low, "alguns blocos do arquivo já foram reaproveitados".to_string()) + ( + Confidence::Low, + "alguns blocos do arquivo já foram reaproveitados".to_string(), + ) }; let nf = NewFound { name: f.name.clone(), @@ -426,7 +497,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { fn covered_by(free: &[Extent], e: &Extent) -> bool { let i = free.partition_point(|f| f.end() <= e.offset); - free.get(i).map(|f| f.offset <= e.offset && f.end() >= e.end()).unwrap_or(false) + free.get(i) + .map(|f| f.offset <= e.offset && f.end() >= e.end()) + .unwrap_or(false) } impl Vol<'_> { @@ -434,7 +507,14 @@ impl Vol<'_> { let mut out = vec![]; for &(s, c) in &f.extents { let n = (c as u64 * self.bs) as usize; - out.extend(read_vec(self.src, self.block_off(s as u64), n.min(max.saturating_sub(out.len()))).ok()?); + out.extend( + read_vec( + self.src, + self.block_off(s as u64), + n.min(max.saturating_sub(out.len())), + ) + .ok()?, + ); if out.len() >= max { break; } @@ -444,8 +524,21 @@ impl Vol<'_> { } /// Procura nós folha do catálogo nas transações guardadas no diário. -fn scan_journal(vol: &Vol, ns: usize, max_id: u32, files: &mut Vec, threads: &mut HashMap, folders: &mut HashMap<(u32, String), u32>, ctx: &Ctx) { - let Some(info) = read_vec(vol.src, vol.block_off(vol.journal_info as u64), 52).ok().filter(|b| b.len() == 52) else { return }; +fn scan_journal( + vol: &Vol, + ns: usize, + max_id: u32, + files: &mut Vec, + threads: &mut HashMap, + folders: &mut HashMap<(u32, String), u32>, + ctx: &Ctx, +) { + let Some(info) = read_vec(vol.src, vol.block_off(vol.journal_info as u64), 52) + .ok() + .filter(|b| b.len() == 52) + else { + return; + }; let (joff, jsize) = (be64(&info, 36), be64(&info, 44)); if jsize == 0 || jsize > 1 << 30 || vol.base + joff + jsize > vol.src.size() { return; @@ -457,14 +550,19 @@ fn scan_journal(vol: &Vol, ns: usize, max_id: u32, files: &mut Vec, thr return; } let n = chunk.min(jsize - pos) as usize; - let Ok(buf) = read_vec(vol.src, vol.base + joff + pos, n + ns) else { break }; + let Ok(buf) = read_vec(vol.src, vol.base + joff + pos, n + ns) else { + break; + }; let mut i = 0; while i + ns <= buf.len().min(n + ns) && i < n { let nd = &buf[i..i + ns]; // nó folha do catálogo: kind = -1, altura 1, registros plausíveis if nd[8] == 0xFF && nd[9] == 1 && be16(nd, 10) > 0 && (be16(nd, 10) as usize) < ns / 8 { let table = ns - 2 * (be16(nd, 10) as usize + 1); - if (be16(nd, ns - 2) as usize) == 14 && be16(nd, table) as usize > 14 && (be16(nd, table) as usize) <= table { + if (be16(nd, ns - 2) as usize) == 14 + && be16(nd, table) as usize > 14 + && (be16(nd, table) as usize) <= table + { for r in records(nd) { decode_record(r, max_id, files, threads, folders, true); } diff --git a/crates/guara-core/src/fs/mod.rs b/crates/guara-core/src/fs/mod.rs index 114e616..30697ae 100644 --- a/crates/guara-core/src/fs/mod.rs +++ b/crates/guara-core/src/fs/mod.rs @@ -10,8 +10,8 @@ pub mod exfat; pub mod ext; pub mod fat; pub mod hfsplus; -pub mod xfs; pub mod ntfs; +pub mod xfs; use crate::signatures::{category_for_ext, matches_ext}; use crate::source::{read_vec, Source}; diff --git a/crates/guara-core/src/fs/xfs.rs b/crates/guara-core/src/fs/xfs.rs index e220ac1..2b30f40 100644 --- a/crates/guara-core/src/fs/xfs.rs +++ b/crates/guara-core/src/fs/xfs.rs @@ -10,13 +10,19 @@ use crate::types::{Confidence, DataLoc, Extent, Origin}; use std::collections::{HashMap, HashSet}; fn be16(b: &[u8], o: usize) -> u16 { - b.get(o..o + 2).map(|s| u16::from_be_bytes([s[0], s[1]])).unwrap_or(0) + b.get(o..o + 2) + .map(|s| u16::from_be_bytes([s[0], s[1]])) + .unwrap_or(0) } fn be32(b: &[u8], o: usize) -> u32 { - b.get(o..o + 4).map(|s| u32::from_be_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 4) + .map(|s| u32::from_be_bytes(s.try_into().unwrap())) + .unwrap_or(0) } fn be64(b: &[u8], o: usize) -> u64 { - b.get(o..o + 8).map(|s| u64::from_be_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 8) + .map(|s| u64::from_be_bytes(s.try_into().unwrap())) + .unwrap_or(0) } struct Xfs<'a> { @@ -46,7 +52,12 @@ impl<'a> Xfs<'a> { let agblocks = be32(&sb, 84) as u64; let agcount = be32(&sb, 88) as u64; let inodesize = be16(&sb, 104) as u64; - if !(512..=65536).contains(&bs) || !bs.is_power_of_two() || agblocks == 0 || agcount == 0 || !(256..=2048).contains(&inodesize) { + if !(512..=65536).contains(&bs) + || !bs.is_power_of_two() + || agblocks == 0 + || agcount == 0 + || !(256..=2048).contains(&inodesize) + { return None; } let incompat = be32(&sb, 216); @@ -63,7 +74,11 @@ impl<'a> Xfs<'a> { agblklog: sb[124] as u32, sectsize: be16(&sb, 102) as u64, v5, - ftype: if v5 { incompat & 1 != 0 } else { features2 & 0x200 != 0 }, + ftype: if v5 { + incompat & 1 != 0 + } else { + features2 & 0x200 != 0 + }, root_ino: be64(&sb, 56), dblocks: be64(&sb, 8), }) @@ -84,7 +99,8 @@ impl<'a> Xfs<'a> { let agno = ino >> shift; let agbno = (ino >> self.inopblog) & ((1 << self.agblklog) - 1); let idx = ino & ((1 << self.inopblog) - 1); - (agno < self.agcount && agbno < self.agblocks).then(|| self.ag_off(agno) + agbno * self.bs + idx * self.inodesize) + (agno < self.agcount && agbno < self.agblocks) + .then(|| self.ag_off(agno) + agbno * self.bs + idx * self.inodesize) } fn read_ino(&self, ino: u64) -> Option> { @@ -104,7 +120,11 @@ impl<'a> Xfs<'a> { fn extents_of(&self, ino: &[u8], max_blocks: u64) -> Vec<(u64, u64, u64)> { let fmt = ino[5]; let start = self.core_size(ino); - let fork_end = if ino[82] != 0 { start.max(100 + ino[82] as usize * 8) } else { self.inodesize as usize }; + let fork_end = if ino[82] != 0 { + start.max(100 + ino[82] as usize * 8) + } else { + self.inodesize as usize + }; let lit = &ino[start..fork_end.min(ino.len())]; let mut out = vec![]; match fmt { @@ -136,7 +156,9 @@ impl<'a> Xfs<'a> { return; } let Some(off) = self.fsb_off(fsb) else { return }; - let Ok(b) = read_vec(self.src, off, self.bs as usize) else { return }; + let Ok(b) = read_vec(self.src, off, self.bs as usize) else { + return; + }; if b.len() < 24 || !(&b[0..4] == b"BMAP" || &b[0..4] == b"BMA3") { return; } @@ -169,11 +191,21 @@ fn unpack_extent(r: &[u8]) -> (u64, u64, u64) { } /// Percorre as folhas de uma árvore B+ curta, entregando cada registro de 16 (inobt) ou 8 (bnobt) bytes. -fn walk_short_btree(x: &Xfs, agno: u64, root: u64, levels: u32, rec: usize, key_size: usize, f: &mut dyn FnMut(&[u8])) { +fn walk_short_btree( + x: &Xfs, + agno: u64, + root: u64, + levels: u32, + rec: usize, + key_size: usize, + f: &mut dyn FnMut(&[u8]), +) { let mut cur = root; // desce pela esquerda até a folha for lvl in (1..levels).rev() { - let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else { return }; + let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else { + return; + }; if b.len() < 24 { return; } @@ -185,7 +217,9 @@ fn walk_short_btree(x: &Xfs, agno: u64, root: u64, levels: u32, rec: usize, key_ } let mut seen = HashSet::new(); while cur != 0 && cur != u32::MAX as u64 && seen.insert(cur) { - let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else { return }; + let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else { + return; + }; if b.len() < 24 { return; } @@ -203,7 +237,9 @@ fn walk_short_btree(x: &Xfs, agno: u64, root: u64, levels: u32, rec: usize, key_ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { let _ = part_len; let mut res = VolumeScan::default(); - let Some(x) = Xfs::open(ctx.src, base) else { return res }; + let Some(x) = Xfs::open(ctx.src, base) else { + return res; + }; // Espaço livre por grupo e inodes livres. let mut free: Vec = vec![]; @@ -237,7 +273,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { free_set.sort(); let is_free = |fsb: u64, count: u64| -> bool { let i = free_set.partition_point(|&(s, c)| s + c <= fsb); - free_set.get(i).map_or(false, |&(s, c)| s <= fsb && s + c >= fsb + count) + free_set + .get(i) + .is_some_and(|&(s, c)| s <= fsb && s + c >= fsb + count) }; // Nomes: entradas de diretório liberadas (guardam o nome e os 4 bytes baixos do inode). @@ -290,16 +328,30 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { continue; } let all_free = ext.iter().all(|e| is_free(e.1, e.2)); - let extents: Vec = ext.iter().filter_map(|e| x.fsb_off(e.1).map(|o| Extent::new(o, e.2 * x.bs))).collect(); + let extents: Vec = ext + .iter() + .filter_map(|e| x.fsb_off(e.1).map(|o| Extent::new(o, e.2 * x.bs))) + .collect(); if extents.is_empty() || !seen_first.insert(extents[0].offset) { continue; } let mut size = total_blocks * x.bs; - let mut conf = if all_free { Confidence::High } else { Confidence::Low }; - let mut note = if all_free { "inode XFS liberado; lista de extents recuperada do próprio inode".to_string() } else { "blocos do arquivo já reaproveitados".to_string() }; + let mut conf = if all_free { + Confidence::High + } else { + Confidence::Low + }; + let mut note = if all_free { + "inode XFS liberado; lista de extents recuperada do próprio inode".to_string() + } else { + "blocos do arquivo já reaproveitados".to_string() + }; // tamanho exato: tenta achar o fim real pelo conteúdo let mut est = true; - if let Some(h) = read_vec(ctx.src, extents[0].offset, 128).ok().and_then(|head| signatures::detect(ctx.src, extents[0].offset, &head, size)) { + if let Some(h) = read_vec(ctx.src, extents[0].offset, 128) + .ok() + .and_then(|head| signatures::detect(ctx.src, extents[0].offset, &head, size)) + { if h.len > 0 && h.len <= size { size = h.len; est = false; @@ -312,7 +364,10 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { let extents = super::fat::trim_extents(extents, size); let low = ino & 0xFFFF_FFFF; let (dir, name) = match stale_names.get(&low) { - Some((parent, n)) => (Some(live_dirs.get(parent).cloned().unwrap_or_default()), n.clone()), + Some((parent, n)) => ( + Some(live_dirs.get(parent).cloned().unwrap_or_default()), + n.clone(), + ), None => (None, format!("inode_{ino}")), }; let ts = |s: u32| (s != 0).then_some(s as i64); @@ -323,7 +378,11 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan { origin: Origin::Xfs, data: DataLoc::Extents { extents }, modified: ts(be32(&raw, 40)), - created: if raw[4] >= 3 { ts(be32(&raw, 144)) } else { None }, + created: if raw[4] >= 3 { + ts(be32(&raw, 144)) + } else { + None + }, accessed: ts(be32(&raw, 32)), base: conf, note: Some(¬e), @@ -345,7 +404,9 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap, HashMap 200_000 { continue; } - let Some(raw) = x.read_ino(dino) else { continue }; + let Some(raw) = x.read_ino(dino) else { + continue; + }; if be16(&raw, 2) & 0o170000 != 0o040000 { continue; } @@ -365,9 +426,14 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap, HashMap (HashMap, HashMap (HashMap, HashMap, stale: &mut HashMap) { +fn parse_dir_block( + x: &Xfs, + b: &[u8], + dino: u64, + path: &str, + queue: &mut Vec<(u64, String)>, + stale: &mut HashMap, +) { let magic = &b[0..4]; let hdr = match magic { b"XD2B" | b"XD2D" => 16, @@ -447,7 +524,9 @@ fn scan_free_region(x: &Xfs, r: &[u8], dir_ino: u64, stale: &mut HashMap= 16 { diff --git a/crates/guara-core/src/image.rs b/crates/guara-core/src/image.rs index 4a5d7eb..21a29aa 100644 --- a/crates/guara-core/src/image.rs +++ b/crates/guara-core/src/image.rs @@ -77,7 +77,12 @@ pub fn create_image_as( ) -> Result { match format { ImageFormat::Raw => create_image(src, dest, cancelled, progress), - ImageFormat::Aff4 { case_number, examiner, evidence_number, notes } => { + ImageFormat::Aff4 { + case_number, + examiner, + evidence_number, + notes, + } => { let opts = crate::aff4::Aff4Options { case_name: case_number.clone(), examiner: examiner.clone(), @@ -85,10 +90,25 @@ pub fn create_image_as( evidence_number: evidence_number.clone(), ..Default::default() }; - let dest = if dest.extension().map_or(false, |e| e.eq_ignore_ascii_case("aff4")) { dest.to_path_buf() } else { dest.with_extension("aff4") }; + let dest = if dest + .extension() + .is_some_and(|e| e.eq_ignore_ascii_case("aff4")) + { + dest.to_path_buf() + } else { + dest.with_extension("aff4") + }; crate::aff4::write_aff4(src, &dest, &opts, cancelled, progress) } - ImageFormat::E01 { case_number, description, evidence_number, examiner, notes, segment_mib, compression } => { + ImageFormat::E01 { + case_number, + description, + evidence_number, + examiner, + notes, + segment_mib, + compression, + } => { let opts = crate::ewf::E01Options { case_number: case_number.clone(), description: description.clone(), diff --git a/crates/guara-core/src/jpegscan.rs b/crates/guara-core/src/jpegscan.rs index cbb3ff8..001c45d 100644 --- a/crates/guara-core/src/jpegscan.rs +++ b/crates/guara-core/src/jpegscan.rs @@ -16,7 +16,12 @@ struct Huff { impl Huff { fn new(counts: &[u8; 16], vals: Vec) -> Huff { - let mut h = Huff { mincode: [0; 17], maxcode: [-1; 18], valptr: [0; 17], vals }; + let mut h = Huff { + mincode: [0; 17], + maxcode: [-1; 18], + valptr: [0; 17], + vals, + }; let mut code = 0i32; let mut k = 0i32; for l in 1..=16 { @@ -103,7 +108,15 @@ pub fn parse_header(d: &[u8]) -> Option
{ if n == 0 || n > 4 || seg.len() < 6 + n * 3 || w == 0 || h == 0 { return None; } - let comps = (0..n).map(|i| (seg[7 + i * 3] >> 4, seg[7 + i * 3] & 15, seg[8 + i * 3] as usize & 3)).collect(); + let comps = (0..n) + .map(|i| { + ( + seg[7 + i * 3] >> 4, + seg[7 + i * 3] & 15, + seg[8 + i * 3] as usize & 3, + ) + }) + .collect(); sof = Some((w, h, comps)); } 0xC2 | 0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => return None, // progressivo, aritmético etc. @@ -136,7 +149,11 @@ pub fn parse_header(d: &[u8]) -> Option
{ return None; } for i in 0..64 { - qt[id][i] = if prec == 0 { seg[q + i] as u16 } else { u16::from_be_bytes([seg[q + 2 * i], seg[q + 2 * i + 1]]) }; + qt[id][i] = if prec == 0 { + seg[q + i] as u16 + } else { + u16::from_be_bytes([seg[q + 2 * i], seg[q + 2 * i + 1]]) + }; } q += n; } @@ -155,7 +172,13 @@ pub fn parse_header(d: &[u8]) -> Option
{ let mut cs = vec![]; for i in 0..ns { let t = seg[2 + i * 2]; - cs.push(Comp { h: comps[i].0, v: comps[i].1, tq: comps[i].2, dc_tbl: (t >> 4) as usize & 3, ac_tbl: (t & 15) as usize & 3 }); + cs.push(Comp { + h: comps[i].0, + v: comps[i].1, + tq: comps[i].2, + dc_tbl: (t >> 4) as usize & 3, + ac_tbl: (t & 15) as usize & 3, + }); } let hmax = cs.iter().map(|c| c.h).max()? as u32; let vmax = cs.iter().map(|c| c.v).max()? as u32; @@ -218,7 +241,15 @@ impl State { self.pred[0] } pub fn start(h: &Header) -> State { - State { pos: h.scan_start, bitbuf: 0, bitcnt: 0, pred: [0; 4], mcu: 0, rst_next: 0, mcus_since_rst: 0 } + State { + pos: h.scan_start, + bitbuf: 0, + bitcnt: 0, + pred: [0; 4], + mcu: 0, + rst_next: 0, + mcus_since_rst: 0, + } } } @@ -240,7 +271,9 @@ struct Bits<'a> { impl Bits<'_> { fn fill(&mut self) { while self.st.bitcnt <= 24 && self.marker.is_none() { - let Some(&b) = self.d.get(self.st.pos) else { break }; + let Some(&b) = self.d.get(self.st.pos) else { + break; + }; if b == 0xFF { match self.d.get(self.st.pos + 1) { Some(0) => { @@ -283,7 +316,10 @@ impl Bits<'_> { return None; } code = (self.st.bitbuf >> (32 - l)) as i32; - if h.maxcode[l as usize] >= 0 && code <= h.maxcode[l as usize] && code >= h.mincode[l as usize] { + if h.maxcode[l as usize] >= 0 + && code <= h.maxcode[l as usize] + && code >= h.mincode[l as usize] + { self.st.bitbuf <<= l; self.st.bitcnt -= l; let idx = h.valptr[l as usize] + code - h.mincode[l as usize]; @@ -321,7 +357,11 @@ pub fn decode_hook( mut hook: Option<&mut dyn FnMut(u32, &[[i32; 64]])>, ) -> Decoded { let mut lblocks: Vec<[i32; 64]> = Vec::new(); - let mut b = Bits { d: data, st: state, marker: None }; + let mut b = Bits { + d: data, + st: state, + marker: None, + }; let total = h.total_mcus(); let mut dc_out = Vec::new(); let mut snaps = Vec::new(); @@ -428,7 +468,12 @@ pub fn decode_hook( last_snap = b.st.pos; } } - Decoded { dc: dc_out, snaps, stop, state: b.st } + Decoded { + dc: dc_out, + snaps, + stop, + state: b.st, + } } fn fail(b: &Bits, data: &[u8]) -> Stop { @@ -496,8 +541,9 @@ pub fn first_discontinuity(h: &Header, dc: &[i32]) -> Option { // --------------------------------------------------------------- pixels const ZIGZAG: [usize; 64] = [ - 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, - 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, + 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, + 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, ]; fn cos_table() -> &'static [[f32; 8]; 8] { @@ -506,8 +552,14 @@ fn cos_table() -> &'static [[f32; 8]; 8] { let mut t = [[0f32; 8]; 8]; for x in 0..8 { for u in 0..8 { - let cu = if u == 0 { std::f32::consts::FRAC_1_SQRT_2 } else { 1.0 }; - t[x][u] = 0.5 * cu * (((2 * x + 1) as f32 * u as f32 * std::f32::consts::PI) / 16.0).cos(); + let cu = if u == 0 { + std::f32::consts::FRAC_1_SQRT_2 + } else { + 1.0 + }; + t[x][u] = 0.5 + * cu + * (((2 * x + 1) as f32 * u as f32 * std::f32::consts::PI) / 16.0).cos(); } } t @@ -556,7 +608,8 @@ pub fn mcu_luma(h: &Header, blocks: &[[i32; 64]]) -> Vec { let px = idct_block(blk, &h.qt[c.tq]); let (bx, by) = (i % bw, i / bw); for y in 0..8 { - out[(by * 8 + y) * w + bx * 8..(by * 8 + y) * w + bx * 8 + 8].copy_from_slice(&px[y * 8..y * 8 + 8]); + out[(by * 8 + y) * w + bx * 8..(by * 8 + y) * w + bx * 8 + 8] + .copy_from_slice(&px[y * 8..y * 8 + 8]); } } out @@ -576,7 +629,14 @@ impl Plane { pub fn new(h: &Header) -> Plane { let c = &h.comps[0]; let (mw, mh) = (c.h as usize * 8, c.v as usize * 8); - Plane { w: mw * h.mcus_x as usize, h: mh * h.mcus_y as usize, px: vec![0; mw * h.mcus_x as usize * mh * h.mcus_y as usize], mw, mh, have: 0 } + Plane { + w: mw * h.mcus_x as usize, + h: mh * h.mcus_y as usize, + px: vec![0; mw * h.mcus_x as usize * mh * h.mcus_y as usize], + mw, + mh, + have: 0, + } } pub fn put(&mut self, h: &Header, mcu: u32, luma: &[u8]) { let (mx, my) = ((mcu % h.mcus_x) as usize, (mcu / h.mcus_x) as usize); @@ -589,7 +649,13 @@ impl Plane { } /// Decodifica e preenche o plano com as luminâncias. -pub fn decode_plane(h: &Header, data: &[u8], state: State, max_mcus: u32, plane: &mut Plane) -> Decoded { +pub fn decode_plane( + h: &Header, + data: &[u8], + state: State, + max_mcus: u32, + plane: &mut Plane, +) -> Decoded { let mut hook = |mcu: u32, blocks: &[[i32; 64]]| { let l = mcu_luma(h, blocks); plane.put(h, mcu, &l); diff --git a/crates/guara-core/src/lib.rs b/crates/guara-core/src/lib.rs index 4b20a5e..732e60c 100644 --- a/crates/guara-core/src/lib.rs +++ b/crates/guara-core/src/lib.rs @@ -1,3 +1,10 @@ +#![allow( + clippy::type_complexity, + clippy::too_many_arguments, + clippy::if_same_then_else, + clippy::large_enum_variant, + clippy::needless_range_loop +)] //! Guará: núcleo de recuperação de arquivos. //! //! Princípio central: a origem é aberta **somente para leitura**; nada neste @@ -6,9 +13,9 @@ pub mod aff4; pub mod assemble; pub mod carve; +pub mod crypt; pub mod defrag; pub mod device; -pub mod crypt; pub mod error; pub mod ewf; pub mod explorer; @@ -26,9 +33,9 @@ pub mod mp4rebuild; pub mod notify; pub mod owner; pub mod partition; -pub mod reconnect; pub mod raid; pub mod raid_detect; +pub mod reconnect; pub mod recover; pub mod report; pub mod scan; diff --git a/crates/guara-core/src/lvm.rs b/crates/guara-core/src/lvm.rs index 77cbc16..fe1fe2a 100644 --- a/crates/guara-core/src/lvm.rs +++ b/crates/guara-core/src/lvm.rs @@ -60,7 +60,9 @@ pub fn read_pv(src: &dyn Source) -> Option { } let mut best: Option = None; for (mda_off, mda_size) in mdas { - let Ok(h) = read_vec(src, mda_off, 512) else { continue }; + let Ok(h) = read_vec(src, mda_off, 512) else { + continue; + }; if h.len() < 512 || &h[4..20] != MDA_MAGIC { continue; } @@ -85,11 +87,25 @@ pub fn read_pv(src: &dyn Source) -> Option { text = [a, b].concat(); } } - let s = String::from_utf8_lossy(&text).trim_end_matches('\0').to_string(); + let s = String::from_utf8_lossy(&text) + .trim_end_matches('\0') + .to_string(); let Some(tree) = parse(&s) else { continue }; - let Some((vg_name, vg)) = tree.sections.iter().find(|(_, sec)| sec.values.contains_key("extent_size")) else { continue }; + let Some((vg_name, vg)) = tree + .sections + .iter() + .find(|(_, sec)| sec.values.contains_key("extent_size")) + else { + continue; + }; let seqno = vg.int("seqno").unwrap_or(0); - let cand = PvInfo { uuid: uuid.clone(), size, seqno, vg_name: vg_name.clone(), text: s.clone() }; + let cand = PvInfo { + uuid: uuid.clone(), + size, + seqno, + vg_name: vg_name.clone(), + text: s.clone(), + }; if best.as_ref().map(|b| cand.seqno > b.seqno).unwrap_or(true) { best = Some(cand); } @@ -133,7 +149,10 @@ impl Section { } } pub fn sub(&self, name: &str) -> Option<&Section> { - self.sections.iter().find(|(n, _)| n == name).map(|(_, s)| s) + self.sections + .iter() + .find(|(n, _)| n == name) + .map(|(_, s)| s) } } @@ -145,7 +164,9 @@ struct Lexer<'a> { impl Lexer<'_> { fn skip(&mut self) { loop { - while self.i < self.b.len() && (self.b[self.i].is_ascii_whitespace() || self.b[self.i] == b',') { + while self.i < self.b.len() + && (self.b[self.i].is_ascii_whitespace() || self.b[self.i] == b',') + { self.i += 1; } if self.i < self.b.len() && self.b[self.i] == b'#' { @@ -160,7 +181,13 @@ impl Lexer<'_> { fn ident(&mut self) -> Option { self.skip(); let s = self.i; - while self.i < self.b.len() && !self.b[self.i].is_ascii_whitespace() && !matches!(self.b[self.i], b'=' | b'{' | b'}' | b'[' | b']' | b'"' | b'#') { + while self.i < self.b.len() + && !self.b[self.i].is_ascii_whitespace() + && !matches!( + self.b[self.i], + b'=' | b'{' | b'}' | b'[' | b']' | b'"' | b'#' + ) + { self.i += 1; } (self.i > s).then(|| String::from_utf8_lossy(&self.b[s..self.i]).to_string()) @@ -196,13 +223,19 @@ impl Lexer<'_> { } _ => { let s = self.i; - while self.i < self.b.len() && (self.b[self.i].is_ascii_digit() || self.b[self.i] == b'-') { + while self.i < self.b.len() + && (self.b[self.i].is_ascii_digit() || self.b[self.i] == b'-') + { self.i += 1; } if self.i == s { return None; } - std::str::from_utf8(&self.b[s..self.i]).ok()?.parse().ok().map(Val::Int) + std::str::from_utf8(&self.b[s..self.i]) + .ok()? + .parse() + .ok() + .map(Val::Int) } } } @@ -237,7 +270,11 @@ impl Lexer<'_> { } pub fn parse(text: &str) -> Option
{ - Lexer { b: text.as_bytes(), i: 0 }.section(true) + Lexer { + b: text.as_bytes(), + i: 0, + } + .section(true) } // ------------------------------------------------------------- montagem @@ -246,15 +283,32 @@ fn dashed(u: &str) -> String { if u.len() != 32 { return u.to_string(); } - format!("{}-{}-{}-{}-{}-{}-{}", &u[0..6], &u[6..10], &u[10..14], &u[14..18], &u[18..22], &u[22..26], &u[26..32]) + format!( + "{}-{}-{}-{}-{}-{}-{}", + &u[0..6], + &u[6..10], + &u[10..14], + &u[14..18], + &u[18..22], + &u[22..26], + &u[26..32] + ) } #[derive(Clone)] enum Map { /// Lê de `src` a partir de `base` (linear). - Linear { src: Option>, base: u64 }, - Striped { srcs: Vec<(Option>, u64)>, stripe: u64 }, - Mirror { legs: Vec<(Option>, u64)> }, + Linear { + src: Option>, + base: u64, + }, + Striped { + srcs: Vec<(Option>, u64)>, + stripe: u64, + }, + Mirror { + legs: Vec<(Option>, u64)>, + }, Raid(Arc), } @@ -395,31 +449,53 @@ impl Ctx<'_> { }) .collect(); if srcs.iter().any(|(s, _)| s.is_none()) { - self.warnings.push(format!("{name}: falta um volume físico; as áreas dele aparecem zeradas")); + self.warnings.push(format!( + "{name}: falta um volume físico; as áreas dele aparecem zeradas" + )); } if srcs.len() == 1 { let (src, base) = srcs.into_iter().next().unwrap(); Map::Linear { src, base } } else { - Map::Striped { srcs, stripe: seg.int("stripe_size").unwrap_or(128) * 512 } + Map::Striped { + srcs, + stripe: seg.int("stripe_size").unwrap_or(128) * 512, + } } } "mirror" => { let st = mk_stripes(seg.list("mirrors").unwrap_or(&[])); let legs = st .iter() - .filter_map(|(pv, e)| self.pvs.get(pv).map(|(s, pe)| (s.clone(), pe + e * self.ext))) + .filter_map(|(pv, e)| { + self.pvs + .get(pv) + .map(|(s, pe)| (s.clone(), pe + e * self.ext)) + }) .collect(); Map::Mirror { legs } } t if t.starts_with("raid") => { let raids = seg.list("raids").unwrap_or(&[]); - let imgs: Vec = raids.iter().filter_map(|v| if let Val::Str(s) = v { Some(s.clone()) } else { None }).collect(); + let imgs: Vec = raids + .iter() + .filter_map(|v| { + if let Val::Str(s) = v { + Some(s.clone()) + } else { + None + } + }) + .collect(); // Pares (rimage, rmeta) ou lista só de imagens. - let data_lvs: Vec<&String> = imgs.iter().filter(|n| n.contains("_rimage_")).collect(); + let data_lvs: Vec<&String> = + imgs.iter().filter(|n| n.contains("_rimage_")).collect(); let mut members: Vec> = vec![]; for n in &data_lvs { - members.push(self.build(n, depth + 1).map(|src| Member { src, data_offset: 0 })); + members.push(self.build(n, depth + 1).map(|src| Member { + src, + data_offset: 0, + })); } let chunk = seg.int("stripe_size").unwrap_or(128) * 512; let (level, layout) = match t { @@ -435,7 +511,8 @@ impl Ctx<'_> { "raid6_nc" | "raid6_n_6" => (Level::R6, Layout::ParityLast), "raid10" => (Level::R10, Layout::LeftSymmetric), other => { - self.warnings.push(format!("{name}: tipo {other} não suportado")); + self.warnings + .push(format!("{name}: tipo {other} não suportado")); return None; } }; @@ -451,7 +528,9 @@ impl Ctx<'_> { } } other => { - self.warnings.push(format!("{name}: tipo de segmento \"{other}\" não suportado")); + self.warnings.push(format!( + "{name}: tipo de segmento \"{other}\" não suportado" + )); return None; } }; @@ -474,27 +553,48 @@ pub fn assemble_vgs(pvs: Vec<(Arc, PvInfo)>) -> Vec { let mut out = vec![]; for (vg_name, list) in by_vg { let newest = list.iter().max_by_key(|(_, p)| p.seqno).unwrap(); - let Some(tree) = parse(&newest.1.text) else { continue }; - let Some(vg) = tree.sub(&vg_name) else { continue }; + let Some(tree) = parse(&newest.1.text) else { + continue; + }; + let Some(vg) = tree.sub(&vg_name) else { + continue; + }; let ext = vg.int("extent_size").unwrap_or(8192) * 512; let mut pvmap = BTreeMap::new(); let mut warnings = vec![]; if let Some(pvsec) = vg.sub("physical_volumes") { for (pvname, def) in &pvsec.sections { let id = def.str("id").unwrap_or("").to_string(); - let found = list.iter().find(|(_, p)| dashed(&p.uuid) == id).map(|(s, _)| s.clone()); + let found = list + .iter() + .find(|(_, p)| dashed(&p.uuid) == id) + .map(|(s, _)| s.clone()); if found.is_none() { warnings.push(format!("volume físico {pvname} ({id}) não foi fornecido")); } - pvmap.insert(pvname.clone(), (found, def.int("pe_start").unwrap_or(2048) * 512)); + pvmap.insert( + pvname.clone(), + (found, def.int("pe_start").unwrap_or(2048) * 512), + ); } } - let Some(lvsec) = vg.sub("logical_volumes") else { continue }; - let mut ctx = Ctx { ext, pvs: pvmap, lvs: lvsec, cache: BTreeMap::new(), warnings }; + let Some(lvsec) = vg.sub("logical_volumes") else { + continue; + }; + let mut ctx = Ctx { + ext, + pvs: pvmap, + lvs: lvsec, + cache: BTreeMap::new(), + warnings, + }; let mut lvs = vec![]; for (name, def) in &lvsec.sections { // Imagens internas do RAID e metadados não são volumes de usuário. - let hidden = def.list("status").map(|l| !l.iter().any(|v| matches!(v, Val::Str(s) if s == "VISIBLE"))).unwrap_or(false); + let hidden = def + .list("status") + .map(|l| !l.iter().any(|v| matches!(v, Val::Str(s) if s == "VISIBLE"))) + .unwrap_or(false); if hidden { continue; } @@ -502,7 +602,11 @@ pub fn assemble_vgs(pvs: Vec<(Arc, PvInfo)>) -> Vec { lvs.push((name.clone(), src)); } } - out.push(VgResult { name: vg_name, lvs, warnings: ctx.warnings }); + out.push(VgResult { + name: vg_name, + lvs, + warnings: ctx.warnings, + }); } out } diff --git a/crates/guara-core/src/mp4rebuild.rs b/crates/guara-core/src/mp4rebuild.rs index 186ae15..3c06f6c 100644 --- a/crates/guara-core/src/mp4rebuild.rs +++ b/crates/guara-core/src/mp4rebuild.rs @@ -6,7 +6,9 @@ use crate::source::{read_vec, Source}; fn be32(b: &[u8], o: usize) -> u32 { - b.get(o..o + 4).map(|s| u32::from_be_bytes(s.try_into().unwrap())).unwrap_or(0) + b.get(o..o + 4) + .map(|s| u32::from_be_bytes(s.try_into().unwrap())) + .unwrap_or(0) } // ------------------------------------------------------------------ bits @@ -45,7 +47,11 @@ impl<'a> Bits<'a> { } fn se(&mut self) -> Option { let k = self.ue()?; - Some(if k % 2 == 1 { (k / 2 + 1) as i32 } else { -((k / 2) as i32) }) + Some(if k % 2 == 1 { + (k / 2 + 1) as i32 + } else { + -((k / 2) as i32) + }) } fn skip(&mut self, n: usize) { self.pos += n; @@ -101,10 +107,18 @@ pub fn parse_sps264(nal: &[u8]) -> Option { } let rbsp = unescape(&nal[1..]); let mut b = Bits::new(&rbsp); - let mut s = Sps264 { profile: b.u(8)? as u8, compat: b.u(8)? as u8, level: b.u(8)? as u8, ..Default::default() }; + let mut s = Sps264 { + profile: b.u(8)? as u8, + compat: b.u(8)? as u8, + level: b.u(8)? as u8, + ..Default::default() + }; b.ue()?; // sps_id let mut chroma = 1; - if matches!(s.profile, 100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135) { + if matches!( + s.profile, + 100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135 + ) { chroma = b.ue()?; if chroma == 3 { b.bit()?; @@ -161,10 +175,8 @@ pub fn parse_sps264(nal: &[u8]) -> Option { s.height = ((2 - s.frame_mbs_only as u32) * h * 16).checked_sub((ct + cb) * sub_h)?; // VUI até o timing_info. if b.bit()? == 1 { - if b.bit()? == 1 { - if b.u(8)? == 255 { - b.skip(32); - } + if b.bit()? == 1 && b.u(8)? == 255 { + b.skip(32); } if b.bit()? == 1 { b.bit()?; @@ -207,10 +219,8 @@ fn slice264(nal: &[u8], sps: &Sps264, pic_order_present: bool) -> Option Option bool { let rbsp = unescape(&nal[1..]); let mut b = Bits::new(&rbsp); - let (Some(_), Some(_), Some(_)) = (b.ue(), b.ue(), b.bit()) else { return false }; + let (Some(_), Some(_), Some(_)) = (b.ue(), b.ue(), b.bit()) else { + return false; + }; b.bit() == Some(1) } @@ -345,7 +363,12 @@ pub fn config_from_mp4(src: &dyn Source, off: u64, max: u64) -> Option Option if let Some(r) = rebuild_with(src, off, avail, None) { return Some(r); } - refs.iter().find_map(|c| rebuild_with(src, off, avail, Some(c))) + refs.iter() + .find_map(|c| rebuild_with(src, off, avail, Some(c))) } -fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaConfig>) -> Option { +fn rebuild_with( + src: &dyn Source, + off: u64, + avail: u64, + preset: Option<&MediaConfig>, +) -> Option { // Átomos antes do mdat. let mut p = 0u64; let mut pre_end = None; @@ -465,7 +499,10 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon pre_end = Some(p); break; } - if !matches!(ty, b"ftyp" | b"free" | b"skip" | b"wide" | b"uuid" | b"pnot" | b"junk" | b"styp") { + if !matches!( + ty, + b"ftyp" | b"free" | b"skip" | b"wide" | b"uuid" | b"pnot" | b"junk" | b"styp" + ) { return None; } let sz = be32(&h, 0) as u64; @@ -477,7 +514,11 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon let pre_len = pre_end?; let (data_start, declared) = mdat_at(src, off + pre_len)?; let hard_end = off + avail.min(src.size() - off); - let data_end_decl = if declared == 0 { hard_end } else { (off + pre_len + declared).min(hard_end) }; + let data_end_decl = if declared == 0 { + hard_end + } else { + (off + pre_len + declared).min(hard_end) + }; // Lê o payload em blocos, interpretando NALs. let mut samples: Vec = vec![]; @@ -485,7 +526,10 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon let mut cache_off = data_start; let read_at = |o: u64, n: usize, cache: &mut Vec, cache_off: &mut u64| -> Option> { if o < *cache_off || o + n as u64 > *cache_off + cache.len() as u64 { - let want = (data_end_decl.saturating_sub(o) as usize).min(8 << 20).max(n).min((hard_end - o) as usize); + let want = (data_end_decl.saturating_sub(o) as usize) + .min(8 << 20) + .max(n) + .min((hard_end - o) as usize); *cache = read_vec(src, o, want).ok()?; *cache_off = o; } @@ -494,15 +538,18 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon }; // Detecta o codec: o que forma a cadeia mais longa de unidades NAL válidas. let chain = |c: Codec| -> usize { - let Ok(head) = read_vec(src, data_start, 64 << 10) else { return 0 }; + let Ok(head) = read_vec(src, data_start, 64 << 10) else { + return 0; + }; let mut p = 0usize; let mut n = 0; while p + 6 <= head.len() { let l = be32(&head, p) as usize; - let ok = l >= 1 && l <= 8 << 20 && match c { - Codec::Avc => avc_nal_ok(head[p + 4]), - Codec::Hevc => hevc_nal_ok(head[p + 4], head[p + 5]), - }; + let ok = (1..=8 << 20).contains(&l) + && match c { + Codec::Avc => avc_nal_ok(head[p + 4]), + Codec::Hevc => hevc_nal_ok(head[p + 4], head[p + 5]), + }; if !ok { break; } @@ -540,7 +587,11 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon match codec { Codec::Avc => { sps264 = c.sps.iter().find_map(|n| parse_sps264(n)); - pic_order_present = c.pps.first().map(|p| pps_pic_order_present(p)).unwrap_or(false); + pic_order_present = c + .pps + .first() + .map(|p| pps_pic_order_present(p)) + .unwrap_or(false); } Codec::Hevc => sps265 = c.sps.iter().find_map(|n| parse_sps_hevc(n)), } @@ -558,10 +609,12 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon 'outer: while pos + 6 <= data_end_decl { let hdr = read_at(pos, 6, &mut cache, &mut cache_off)?; let len = be32(&hdr, 0) as u64; - let ok = len >= 1 && len <= 16 << 20 && pos + 4 + len <= data_end_decl && match codec { - Codec::Avc => avc_nal_ok(hdr[4]), - Codec::Hevc => hevc_nal_ok(hdr[4], hdr[5]), - }; + let ok = (1..=16 << 20).contains(&len) + && pos + 4 + len <= data_end_decl + && match codec { + Codec::Avc => avc_nal_ok(hdr[4]), + Codec::Hevc => hevc_nal_ok(hdr[4], hdr[5]), + }; let mut ok = ok; if ok && nal_count >= 3 && len > (max_len * 6).max(1 << 20) { // Um "tamanho" muito maior que os já vistos é lixo (áudio lido como vídeo). @@ -577,7 +630,16 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon } if !valid { // Pode ser áudio intercalado: procura a próxima unidade NAL plausível. - let found = scan_nal(src, codec, pos + 1, (pos + (2 << 20)).min(data_end_decl), data_end_decl, sps264.as_ref(), pps_nals.len(), (max_len * 6).max(1 << 20)); + let found = scan_nal( + src, + codec, + pos + 1, + (pos + (2 << 20)).min(data_end_decl), + data_end_decl, + sps264.as_ref(), + pps_nals.len(), + (max_len * 6).max(1 << 20), + ); match found { Some(q) if nal_count > 0 => { // O que veio antes fecha o quadro atual. @@ -593,20 +655,32 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon } } let nal_start = pos + 4; - let nal = read_at(nal_start, len.min(64 << 10) as usize, &mut cache, &mut cache_off)?; + let nal = read_at( + nal_start, + len.min(64 << 10) as usize, + &mut cache, + &mut cache_off, + )?; let (is_vcl, first_of_pic, is_idr, ty) = match codec { Codec::Avc => { let t = nal[0] & 0x1F; match t { 1 | 2 | 5 => { - let first = sps264.as_ref().and_then(|s| slice264(&nal, s, pic_order_present)); - (true, first.as_ref().map(|f| f.first_mb0).unwrap_or(true), t == 5, t) + let first = sps264 + .as_ref() + .and_then(|s| slice264(&nal, s, pic_order_present)); + ( + true, + first.as_ref().map(|f| f.first_mb0).unwrap_or(true), + t == 5, + t, + ) } 7 => { if sps264.is_none() { sps264 = parse_sps264(&nal); } - if !sps_nals.iter().any(|x| *x == nal) && sps_nals.len() < 4 { + if !sps_nals.contains(&nal) && sps_nals.len() < 4 { sps_nals.push(nal.clone()); } (false, false, false, t) @@ -615,7 +689,7 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon if pps_nals.is_empty() { pic_order_present = pps_pic_order_present(&nal); } - if !pps_nals.iter().any(|x| *x == nal) && pps_nals.len() < 4 { + if !pps_nals.contains(&nal) && pps_nals.len() < 4 { pps_nals.push(nal.clone()); } (false, false, false, t) @@ -626,9 +700,14 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon Codec::Hevc => { let t = (nal[0] >> 1) & 0x3F; match t { - 0..=9 | 16..=21 => (true, nal.get(2).map(|b| b & 0x80 != 0).unwrap_or(true), (16..=21).contains(&t), t), + 0..=9 | 16..=21 => ( + true, + nal.get(2).map(|b| b & 0x80 != 0).unwrap_or(true), + (16..=21).contains(&t), + t, + ), 32 => { - if !vps_nals.iter().any(|x| *x == nal) && vps_nals.len() < 2 { + if !vps_nals.contains(&nal) && vps_nals.len() < 2 { vps_nals.push(nal.clone()); } (false, false, false, t) @@ -637,13 +716,13 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon if sps265.is_none() { sps265 = parse_sps_hevc(&nal); } - if !sps_nals.iter().any(|x| *x == nal) && sps_nals.len() < 4 { + if !sps_nals.contains(&nal) && sps_nals.len() < 4 { sps_nals.push(nal.clone()); } (false, false, false, t) } 34 => { - if !pps_nals.iter().any(|x| *x == nal) && pps_nals.len() < 4 { + if !pps_nals.contains(&nal) && pps_nals.len() < 4 { pps_nals.push(nal.clone()); } (false, false, false, t) @@ -695,7 +774,13 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon } } } - cur = Some(Sample { off: start, size: 0, sync: is_idr, poc, idr: is_idr }); + cur = Some(Sample { + off: start, + size: 0, + sync: is_idr, + poc, + idr: is_idr, + }); } if !first_of_pic { group_start = None; @@ -736,7 +821,11 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon None => (30, 1), }; let fps = num as f64 / den as f64; - let (num, den) = if !(1.0..=240.0).contains(&fps) { (30, 1) } else { (num, den) }; + let (num, den) = if !(1.0..=240.0).contains(&fps) { + (30, 1) + } else { + (num, den) + }; let fps = num as f64 / den as f64; // Escala: tique do quadro = den, escala de tempo = num. let (timescale, dur) = (num, den); @@ -766,12 +855,30 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon ctts = Some(offs.iter().map(|&d| (d + depth) * dur as i64).collect()); } } - let reorder_delay: i64 = ctts.as_ref().map(|c| c.iter().copied().min().unwrap_or(0)).unwrap_or(0); + let reorder_delay: i64 = ctts + .as_ref() + .map(|c| c.iter().copied().min().unwrap_or(0)) + .unwrap_or(0); let payload = (data_start, payload_end); let out_payload_start = pre_len + 16; let map_off = |abs: u64| -> u64 { out_payload_start + (abs - data_start) }; - let moov = build_moov(codec, width, height, &sps_nals, &pps_nals, &vps_nals, profile_info.as_ref(), &sps264, &samples, timescale, dur, ctts.as_deref(), reorder_delay, &map_off); + let moov = build_moov( + codec, + width, + height, + &sps_nals, + &pps_nals, + &vps_nals, + profile_info.as_ref(), + &sps264, + &samples, + timescale, + dur, + ctts.as_deref(), + reorder_delay, + &map_off, + ); let used: u64 = samples.iter().map(|s| s.size as u64).sum(); Some(Rebuilt { pre_len, @@ -846,7 +953,14 @@ fn build_moov( let (fourcc, cfg) = match codec { Codec::Avc => { let s = avc.as_ref().unwrap(); - let mut c = vec![1, s.profile, s.compat, s.level, 0xFF, 0xE0 | sps.len() as u8]; + let mut c = vec![ + 1, + s.profile, + s.compat, + s.level, + 0xFF, + 0xE0 | sps.len() as u8, + ]; for x in sps { c.extend_from_slice(&(x.len() as u16).to_be_bytes()); c.extend_from_slice(x); @@ -893,14 +1007,24 @@ fn build_moov( stsd_body.extend_from_slice(&sample_entry); let stsd = full(b"stsd", 0, 0, &stsd_body); - let stts = full(b"stts", 0, 0, &[u32b(1).as_slice(), &u32b(n as u32), &u32b(dur)].concat()); + let stts = full( + b"stts", + 0, + 0, + &[u32b(1).as_slice(), &u32b(n as u32), &u32b(dur)].concat(), + ); let mut stsz_body = u32b(0).to_vec(); stsz_body.extend_from_slice(&u32b(n as u32)); for s in samples { stsz_body.extend_from_slice(&u32b(s.size)); } let stsz = full(b"stsz", 0, 0, &stsz_body); - let stsc = full(b"stsc", 0, 0, &[u32b(1).as_slice(), &u32b(1), &u32b(1), &u32b(1)].concat()); + let stsc = full( + b"stsc", + 0, + 0, + &[u32b(1).as_slice(), &u32b(1), &u32b(1), &u32b(1)].concat(), + ); let need64 = samples.iter().any(|s| map_off(s.off) > u32::MAX as u64); let stco = if need64 { let mut b = u32b(n as u32).to_vec(); @@ -915,7 +1039,12 @@ fn build_moov( } full(b"stco", 0, 0, &b) }; - let syncs: Vec = samples.iter().enumerate().filter(|(_, s)| s.sync).map(|(i, _)| i as u32 + 1).collect(); + let syncs: Vec = samples + .iter() + .enumerate() + .filter(|(_, s)| s.sync) + .map(|(i, _)| i as u32 + 1) + .collect(); let mut stbl_parts = vec![stsd, stts]; if let Some(c) = ctts { // entradas agrupadas @@ -1008,7 +1137,13 @@ fn build_moov( } /// A unidade NAL parece de verdade? Valida o cabeçalho de fatia e o tamanho de SPS/PPS/AUD. -fn nal_plausible(codec: Codec, head: &[u8], len: u64, sps: Option<&Sps264>, pps_count: usize) -> bool { +fn nal_plausible( + codec: Codec, + head: &[u8], + len: u64, + sps: Option<&Sps264>, + pps_count: usize, +) -> bool { match codec { Codec::Avc => { let t = head[0] & 0x1F; @@ -1016,8 +1151,13 @@ fn nal_plausible(codec: Codec, head: &[u8], len: u64, sps: Option<&Sps264>, pps_ 1 | 2 | 5 => { let rbsp = unescape(&head[1..]); let mut b = Bits::new(&rbsp); - let (Some(first), Some(st), Some(pps)) = (b.ue(), b.ue(), b.ue()) else { return false }; - if st > 9 || pps > 255 || (pps_count > 0 && pps as usize >= pps_count.max(1) && pps > 3) { + let (Some(first), Some(st), Some(pps)) = (b.ue(), b.ue(), b.ue()) else { + return false; + }; + if st > 9 + || pps > 255 + || (pps_count > 0 && pps as usize >= pps_count.max(1) && pps > 3) + { return false; } if t == 5 && !matches!(st, 2 | 4 | 7 | 9) { @@ -1047,7 +1187,7 @@ fn nal_plausible(codec: Codec, head: &[u8], len: u64, sps: Option<&Sps264>, pps_ let t = (head[0] >> 1) & 0x3F; match t { 35 => len <= 4, - 32 | 33 | 34 => (4..=1000).contains(&len), + 32..=34 => (4..=1000).contains(&len), _ => true, } } @@ -1055,17 +1195,34 @@ fn nal_plausible(codec: Codec, head: &[u8], len: u64, sps: Option<&Sps264>, pps_ } /// Quantas unidades NAL válidas seguidas (sem vãos) começam em `at` (até `max`). -fn chain_len(buf: &[u8], base: u64, at: u64, codec: Codec, sps: Option<&Sps264>, pps_count: usize, cap: u64, max: usize) -> usize { +fn chain_len( + buf: &[u8], + base: u64, + at: u64, + codec: Codec, + sps: Option<&Sps264>, + pps_count: usize, + cap: u64, + max: usize, +) -> usize { let mut p = (at - base) as usize; let mut n = 0; while n < max && p + 8 <= buf.len() { let l = be32(buf, p) as u64; - let ok = l >= 1 && l <= cap && p as u64 + 4 + l <= buf.len() as u64 + 8 + let ok = l >= 1 + && l <= cap + && p as u64 + 4 + l <= buf.len() as u64 + 8 && match codec { Codec::Avc => avc_nal_ok(buf[p + 4]), Codec::Hevc => hevc_nal_ok(buf[p + 4], buf[p + 5]), } - && nal_plausible(codec, &buf[p + 4..(p + 4 + (l as usize).min(48)).min(buf.len())], l, sps, pps_count); + && nal_plausible( + codec, + &buf[p + 4..(p + 4 + (l as usize).min(48)).min(buf.len())], + l, + sps, + pps_count, + ); if !ok { break; } @@ -1077,12 +1234,22 @@ fn chain_len(buf: &[u8], base: u64, at: u64, codec: Codec, sps: Option<&Sps264>, /// Próxima posição em [from, limit) onde começa uma sequência de unidades NAL plausíveis. /// Prefere o primeiro ponto com duas ou mais unidades seguidas; senão, uma unidade isolada. -fn scan_nal(src: &dyn Source, codec: Codec, from: u64, limit: u64, data_end: u64, sps: Option<&Sps264>, pps_count: usize, cap: u64) -> Option { +fn scan_nal( + src: &dyn Source, + codec: Codec, + from: u64, + limit: u64, + data_end: u64, + sps: Option<&Sps264>, + pps_count: usize, + cap: u64, +) -> Option { if limit <= from + 8 { return None; } // Lê um pouco além do limite para poder seguir a cadeia de unidades. - let want = ((limit - from) as usize + (cap as usize).min(4 << 20)).min((data_end - from) as usize); + let want = + ((limit - from) as usize + (cap as usize).min(4 << 20)).min((data_end - from) as usize); let buf = read_vec(src, from, want).ok()?; let mut single = None; let mut i = 0usize; @@ -1093,7 +1260,11 @@ fn scan_nal(src: &dyn Source, codec: Codec, from: u64, limit: u64, data_end: u64 let nb = buf[i + 4]; let good_head = match codec { // Um quadro novo começa por parâmetros/AUD/SEI ou por fatia do macrobloco 0. - Codec::Avc => avc_nal_ok(nb) && (matches!(nb & 0x1F, 6..=9) || (matches!(nb & 0x1F, 1 | 5) && buf[i + 5] & 0x80 != 0)), + Codec::Avc => { + avc_nal_ok(nb) + && (matches!(nb & 0x1F, 6..=9) + || (matches!(nb & 0x1F, 1 | 5) && buf[i + 5] & 0x80 != 0)) + } Codec::Hevc => hevc_nal_ok(nb, buf[i + 5]), }; if good_head { diff --git a/crates/guara-core/src/partition.rs b/crates/guara-core/src/partition.rs index 9c0f4bb..d2f1103 100644 --- a/crates/guara-core/src/partition.rs +++ b/crates/guara-core/src/partition.rs @@ -19,7 +19,10 @@ pub fn detect_fs(src: &dyn Source, start: u64) -> Option<(FsKind, Option)> // Assinaturas mais adiante no volume: Btrfs (64 KiB) e RAID mdadm 1.2 (4 KiB). if let Ok(h) = read_vec(src, start + 0x10000, 0x100) { if h.len() >= 0x80 && &h[0x40..0x48] == b"_BHRfS_M" { - return Some((FsKind::Btrfs, Some(u64::from_le_bytes(h[0x70..0x78].try_into().unwrap())))); + return Some(( + FsKind::Btrfs, + Some(u64::from_le_bytes(h[0x70..0x78].try_into().unwrap())), + )); } } if let Ok(h) = read_vec(src, start + 4096, 16) { @@ -68,7 +71,10 @@ pub fn detect_fs_in(b: &[u8]) -> Option<(FsKind, Option)> { return Some((FsKind::Refs, Some(sectors * bps))); } if crate::crypt::filevault::detect(b) { - return Some((FsKind::Corestorage, Some(u64::from_le_bytes(b[64..72].try_into().unwrap())))); + return Some(( + FsKind::Corestorage, + Some(u64::from_le_bytes(b[64..72].try_into().unwrap())), + )); } if crate::crypt::bitlocker::detect(b) { return Some((FsKind::Bitlocker, None)); diff --git a/crates/guara-core/src/raid.rs b/crates/guara-core/src/raid.rs index 09798d4..ce98963 100644 --- a/crates/guara-core/src/raid.rs +++ b/crates/guara-core/src/raid.rs @@ -60,7 +60,12 @@ impl Layout { } } pub fn all_raid5() -> [Layout; 4] { - [Layout::LeftSymmetric, Layout::LeftAsymmetric, Layout::RightSymmetric, Layout::RightAsymmetric] + [ + Layout::LeftSymmetric, + Layout::LeftAsymmetric, + Layout::RightSymmetric, + Layout::RightAsymmetric, + ] } } @@ -89,7 +94,16 @@ pub struct RaidParams { impl RaidParams { pub fn simple(level: Level, chunk: u64, layout: Layout, members: Vec>) -> Self { - Self { level, chunk, layout, members, near: 2, far: 1, far_offset: false, per_member: 0 } + Self { + level, + chunk, + layout, + members, + near: 2, + far: 1, + far_offset: false, + per_member: 0, + } } } @@ -149,7 +163,11 @@ impl RaidSource { let present: Vec<&Member> = p.members.iter().flatten().collect(); let min = |f: &dyn Fn(&Member) -> u64| present.iter().map(|m| f(m)).min().unwrap_or(0); let avail = min(&|m| m.src.size().saturating_sub(m.data_offset)); - let mut per_member = if p.per_member > 0 { p.per_member.min(avail) } else { avail }; + let mut per_member = if p.per_member > 0 { + p.per_member.min(avail) + } else { + avail + }; if p.level != Level::Linear { per_member = per_member / p.chunk.max(512) * p.chunk.max(512); } @@ -162,7 +180,10 @@ impl RaidSource { Level::R10 => (p.near.max(1) as usize).saturating_sub(1).min(1), }; if missing > need && p.level != Level::R10 { - return Err(format!("faltam {missing} membro(s); {} tolera no máximo {need}", p.level.name())); + return Err(format!( + "faltam {missing} membro(s); {} tolera no máximo {need}", + p.level.name() + )); } if matches!(p.level, Level::R4 | Level::R5) && n < 3 || p.level == Level::R6 && n < 4 { return Err("poucos membros para o nível".into()); @@ -171,14 +192,21 @@ impl RaidSource { return Err("parâmetros de RAID 10 inválidos".into()); } let size = match p.level { - Level::Linear => present.iter().map(|m| m.src.size().saturating_sub(m.data_offset)).sum(), + Level::Linear => present + .iter() + .map(|m| m.src.size().saturating_sub(m.data_offset)) + .sum(), Level::R0 => per_member * n as u64, Level::R1 => per_member, Level::R4 | Level::R5 => per_member * (n as u64 - 1), Level::R6 => per_member * (n as u64 - 2), Level::R10 => per_member * n as u64 / (p.near.max(1) * p.far.max(1)) as u64, }; - Ok(Self { p, size, per_member }) + Ok(Self { + p, + size, + per_member, + }) } pub fn params(&self) -> &RaidParams { @@ -186,10 +214,14 @@ impl RaidSource { } fn read_member(&self, i: usize, off: u64, buf: &mut [u8]) -> io::Result<()> { - let m = self.p.members[i].as_ref().ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "membro ausente"))?; + let m = self.p.members[i] + .as_ref() + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "membro ausente"))?; let mut done = 0; while done < buf.len() { - let n = m.src.read_at(m.data_offset + off + done as u64, &mut buf[done..])?; + let n = m + .src + .read_at(m.data_offset + off + done as u64, &mut buf[done..])?; if n == 0 { buf[done..].fill(0); break; @@ -206,12 +238,24 @@ impl RaidSource { match self.p.layout { Layout::LeftAsymmetric | Layout::LeftSymmetric => { let pd = n - 1 - r; - let disk = if self.p.layout == Layout::LeftSymmetric { (pd + 1 + d) % n } else if d >= pd { d + 1 } else { d }; + let disk = if self.p.layout == Layout::LeftSymmetric { + (pd + 1 + d) % n + } else if d >= pd { + d + 1 + } else { + d + }; (pd, disk) } Layout::RightAsymmetric | Layout::RightSymmetric => { let pd = r; - let disk = if self.p.layout == Layout::RightSymmetric { (pd + 1 + d) % n } else if d >= pd { d + 1 } else { d }; + let disk = if self.p.layout == Layout::RightSymmetric { + (pd + 1 + d) % n + } else if d >= pd { + d + 1 + } else { + d + }; (pd, disk) } Layout::ParityFirst => (0, d + 1), @@ -273,11 +317,13 @@ impl RaidSource { } Level::R1 => { for i in 0..n { - if self.p.members[i].is_some() && self.read_member(i, chunk_idx * c + within, buf).is_ok() { + if self.p.members[i].is_some() + && self.read_member(i, chunk_idx * c + within, buf).is_ok() + { return Ok(()); } } - Err(io::Error::new(io::ErrorKind::Other, "nenhuma cópia legível")) + Err(io::Error::other("nenhuma cópia legível")) } Level::R4 | Level::R5 => { let dd = n - 1; @@ -322,7 +368,8 @@ impl RaidSource { } } } - Err(last.unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "cópias ausentes"))) + Err(last + .unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "cópias ausentes"))) } } } @@ -338,7 +385,11 @@ impl RaidSource { if self.p.far_offset { stripe *= far; } - let stride = if self.p.far_offset { c } else { (self.per_member / far) / c * c }; + let stride = if self.p.far_offset { + c + } else { + (self.per_member / far) / c * c + }; let mut out = vec![]; let mut d = dev; let mut off = stripe * c; @@ -367,7 +418,9 @@ impl RaidSource { let len = buf.len(); // Estado de cada dado da linha. let disks: Vec = (0..dd).map(|k| self.raid6_map(row, k).2).collect(); - let missing_data: Vec = (0..dd).filter(|&k| self.p.members[disks[k]].is_none()).collect(); + let missing_data: Vec = (0..dd) + .filter(|&k| self.p.members[disks[k]].is_none()) + .collect(); let p_ok = self.p.members[pd].is_some(); let q_ok = self.p.members[qd].is_some(); let mut data: Vec>> = vec![None; dd]; @@ -437,7 +490,9 @@ impl RaidSource { } return Ok(()); } - Err(io::Error::new(io::ErrorKind::Other, "RAID 6 sem redundância suficiente nesta linha")) + Err(io::Error::other( + "RAID 6 sem redundância suficiente nesta linha", + )) } } @@ -467,7 +522,9 @@ impl Source for RaidSource { if offset + done as u64 >= base && offset + (done as u64) < base + len { let rel = offset + done as u64 - base; let n = ((len - rel) as usize).min(want - done); - let got = m.src.read_at(m.data_offset + rel, &mut buf[done..done + n])?; + let got = m + .src + .read_at(m.data_offset + rel, &mut buf[done..done + n])?; if got == 0 { break; } @@ -541,7 +598,9 @@ pub fn read_md_super(src: &dyn Source) -> Option { cands.push(((size & !4095) - 8192, "1.0")); } for (off, ver) in cands { - let Ok(b) = read_vec(src, off, 4096) else { continue }; + let Ok(b) = read_vec(src, off, 4096) else { + continue; + }; if b.len() < 4096 || le32(&b, 0) != 0xA92B_4EFC || le32(&b, 4) != 1 { continue; } @@ -556,7 +615,9 @@ pub fn read_md_super(src: &dyn Source) -> Option { }; let mut uuid = [0u8; 16]; uuid.copy_from_slice(&b[16..32]); - let name = String::from_utf8_lossy(&b[32..64]).trim_end_matches('\0').to_string(); + let name = String::from_utf8_lossy(&b[32..64]) + .trim_end_matches('\0') + .to_string(); let data_offset = le64(&b, 128) * 512; let data_size = le64(&b, 136) * 512; let raid_disks = le32(&b, 92); @@ -661,12 +722,20 @@ pub fn assemble_md(members: Vec<(Arc, MdSuper)>) -> Vec> 8) & 0xff; diff --git a/crates/guara-core/src/raid_detect.rs b/crates/guara-core/src/raid_detect.rs index fe4f0cf..e6b0658 100644 --- a/crates/guara-core/src/raid_detect.rs +++ b/crates/guara-core/src/raid_detect.rs @@ -23,13 +23,23 @@ impl Candidate { self.level, self.chunk, self.layout, - self.order.iter().map(|&i| Some(Member { src: members[i].clone(), data_offset: 0 })).collect(), + self.order + .iter() + .map(|&i| { + Some(Member { + src: members[i].clone(), + data_offset: 0, + }) + }) + .collect(), ) } } const PREFIX: u64 = 24 << 20; -const CHUNKS: [u64; 9] = [4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576]; +const CHUNKS: [u64; 9] = [ + 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, +]; fn permutations(n: usize) -> Vec> { fn rec(cur: &mut Vec, used: &mut Vec, n: usize, out: &mut Vec>) { @@ -53,7 +63,9 @@ fn permutations(n: usize) -> Vec> { } fn ntfs_score(src: &dyn Source, start: u64) -> f64 { - let Ok(b) = read_vec(src, start, 512) else { return 0.0 }; + let Ok(b) = read_vec(src, start, 512) else { + return 0.0; + }; if b.len() < 512 || &b[3..11] != b"NTFS " { return 0.0; } @@ -61,18 +73,26 @@ fn ntfs_score(src: &dyn Source, start: u64) -> f64 { let spc = b[13] as u64; let mft = u64::from_le_bytes(b[0x30..0x38].try_into().unwrap()); let rs = b[0x40] as i8; - let rec = if rs > 0 { rs as u64 * bps * spc } else { 1u64 << (-rs) }; + let rec = if rs > 0 { + rs as u64 * bps * spc + } else { + 1u64 << (-rs) + }; if bps == 0 || spc == 0 || rec == 0 || rec > 65536 { return 0.0; } let mft_off = start + mft * bps * spc; let (mut ok, mut total) = (0, 0); - for k in [0u64, 1, 2, 3, 5, 11, 16, 24, 40, 100, 250, 600, 1500, 3000, 6000, 12000, 24000, 50000] { + for k in [ + 0u64, 1, 2, 3, 5, 11, 16, 24, 40, 100, 250, 600, 1500, 3000, 6000, 12000, 24000, 50000, + ] { let off = mft_off + k * rec; if off + rec > src.size() { break; } - let Ok(r) = read_vec(src, off, 64) else { continue }; + let Ok(r) = read_vec(src, off, 64) else { + continue; + }; if r.len() < 64 { continue; } @@ -106,18 +126,25 @@ pub fn fs_score(src: &dyn Source) -> f64 { let layout = partition::analyze(src, false); let mut best = 0.0f64; for p in &layout.partitions { - best = best.max(volume_score(src, p.start, p.fs) + if p.fs != FsKind::Unknown { 0.5 } else { 0.0 }); + best = best.max( + volume_score(src, p.start, p.fs) + if p.fs != FsKind::Unknown { 0.5 } else { 0.0 }, + ); } best } fn first_sector_plausible(src: &dyn Source) -> bool { - let Ok(b) = read_vec(src, 0, 4096) else { return false }; + let Ok(b) = read_vec(src, 0, 4096) else { + return false; + }; b.len() >= 1100 && ((b[510] == 0x55 && b[511] == 0xAA) || partition::detect_fs_in(&b).is_some()) } /// Testa as combinações e devolve as melhores (maior pontuação primeiro). -pub fn detect(members: &[Arc], cancelled: &(dyn Fn() -> bool + Sync)) -> Vec { +pub fn detect( + members: &[Arc], + cancelled: &(dyn Fn() -> bool + Sync), +) -> Vec { let n = members.len(); if n < 2 { return vec![]; @@ -140,13 +167,22 @@ pub fn detect(members: &[Arc], cancelled: &(dyn Fn() -> bool + Sync) let probe = |s: &Arc| read_vec(s.as_ref(), 0, 1 << 20).unwrap_or_default(); let first = probe(&cached[0]); if first.iter().any(|&x| x != 0) && cached.iter().skip(1).all(|c| probe(c) == first) { - let c = Candidate { level: Level::R1, chunk: 65536, layout: Layout::LeftSymmetric, order: (0..n).collect(), score: fs_score(cached[0].as_ref()) + 1.0 }; + let c = Candidate { + level: Level::R1, + chunk: 65536, + layout: Layout::LeftSymmetric, + order: (0..n).collect(), + score: fs_score(cached[0].as_ref()) + 1.0, + }; out.push(c); } // RAID 5/4 satisfaz "XOR de todos os discos = 0" em toda faixa. let sample = 4usize << 20; - let bufs: Vec> = cached.iter().map(|c| read_vec(c.as_ref(), 0, sample).unwrap_or_default()).collect(); + let bufs: Vec> = cached + .iter() + .map(|c| read_vec(c.as_ref(), 0, sample).unwrap_or_default()) + .collect(); let mut xor_zero = 0usize; let mut nonzero_rows = 0usize; for blk in 0..sample / 4096 { @@ -181,20 +217,35 @@ pub fn detect(members: &[Arc], cancelled: &(dyn Fn() -> bool + Sync) levels.push(Level::R6); } } - let ok_first: Vec = cached.iter().map(|c| first_sector_plausible(c.as_ref())).collect(); + let ok_first: Vec = cached + .iter() + .map(|c| first_sector_plausible(c.as_ref())) + .collect(); let any_first = ok_first.iter().any(|&b| b); let perms = permutations(n); for level in levels { - let layouts: Vec = if matches!(level, Level::R0) { vec![Layout::LeftSymmetric] } else { Layout::all_raid5().to_vec() }; + let layouts: Vec = if matches!(level, Level::R0) { + vec![Layout::LeftSymmetric] + } else { + Layout::all_raid5().to_vec() + }; for &chunk in &CHUNKS { for layout in &layouts { for order in &perms { if cancelled() { return finish(out); } - let cand = Candidate { level, chunk, layout: *layout, order: order.clone(), score: 0.0 }; + let cand = Candidate { + level, + chunk, + layout: *layout, + order: order.clone(), + score: 0.0, + }; let params = cand.params(&cached); - let Ok(r) = RaidSource::new(params) else { continue }; + let Ok(r) = RaidSource::new(params) else { + continue; + }; // O primeiro setor do volume precisa estar num disco que o tenha. if any_first { let mut b = [0u8; 512]; @@ -221,7 +272,11 @@ pub fn detect(members: &[Arc], cancelled: &(dyn Fn() -> bool + Sync) } fn finish(mut out: Vec) -> Vec { - out.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); + out.sort_by(|a, b| { + b.score + .partial_cmp(&a.score) + .unwrap_or(std::cmp::Ordering::Equal) + }); out.truncate(10); out } diff --git a/crates/guara-core/src/scan.rs b/crates/guara-core/src/scan.rs index 7ada9c5..8fc7b3d 100644 --- a/crates/guara-core/src/scan.rs +++ b/crates/guara-core/src/scan.rs @@ -274,8 +274,12 @@ impl Handler<'_> { refs.push(c); } } - let Some(r) = crate::mp4rebuild::rebuild(self.src, off, p.hit_len, &refs) else { return }; - let Ok(pre) = crate::source::read_vec(self.src, off, r.pre_len as usize) else { return }; + let Some(r) = crate::mp4rebuild::rebuild(self.src, off, p.hit_len, &refs) else { + return; + }; + let Ok(pre) = crate::source::read_vec(self.src, off, r.pre_len as usize) else { + return; + }; let payload_len = r.payload.1 - r.payload.0; let mut f = p.found; let hdr = r.mdat_header(); @@ -284,7 +288,10 @@ impl Handler<'_> { parts: vec![ Part::Bytes { bytes: pre }, Part::Bytes { bytes: hdr }, - Part::Run { offset: r.payload.0, len: payload_len }, + Part::Run { + offset: r.payload.0, + len: payload_len, + }, Part::Bytes { bytes: r.moov }, ], }; @@ -311,13 +318,23 @@ impl Handler<'_> { used.extend(self.carved.iter().copied()); // O próprio começo do arquivo não conta como "ocupado" para ele mesmo. used.retain(|e| !(e.offset == off)); - let pool = crate::defrag::Pool { index: &self.index, used: &used }; - if let crate::defrag::Analysis::Rebuilt { extents, size } = crate::defrag::analyze_jpeg(self.src, off, p.hit_len, &pool, true) { + let pool = crate::defrag::Pool { + index: &self.index, + used: &used, + }; + if let crate::defrag::Analysis::Rebuilt { extents, size } = + crate::defrag::analyze_jpeg(self.src, off, p.hit_len, &pool, true) + { let mut f = p.found; f.size = size; - f.data = DataLoc::Extents { extents: extents.clone() }; + f.data = DataLoc::Extents { + extents: extents.clone(), + }; f.confidence = Confidence::Medium; - f.note = Some(format!("remontado a partir de {} fragmentos separados no disco; confira a imagem", extents.len())); + f.note = Some(format!( + "remontado a partir de {} fragmentos separados no disco; confira a imagem", + extents.len() + )); self.carved.extend(extents); self.sink.replace(f); } @@ -354,7 +371,10 @@ impl CarveEvents for Handler<'_> { let mut skip_len = None; let mut pending_len = None; if hit.ext == "jpg" { - let pool = crate::defrag::Pool { index: &self.index, used: &[] }; + let pool = crate::defrag::Pool { + index: &self.index, + used: &[], + }; match crate::defrag::analyze_jpeg(self.src, off, hit.len, &pool, false) { crate::defrag::Analysis::Intact { len } => { hit.len = len; @@ -412,8 +432,16 @@ impl CarveEvents for Handler<'_> { self.carved.push(Extent::new(off, hit.len)); if let Some(hl) = pending_len { let mut pf = f.clone(); - pf.note = Some(if pending_mp4 { "vídeo sem índice (moov)".into() } else { "JPEG interrompido ou fragmentado".into() }); - self.pending.push(Pending { found: pf, hit_len: hl, mp4: pending_mp4 }); + pf.note = Some(if pending_mp4 { + "vídeo sem índice (moov)".into() + } else { + "JPEG interrompido ou fragmentado".into() + }); + self.pending.push(Pending { + found: pf, + hit_len: hl, + mp4: pending_mp4, + }); } self.sink.found(f); match skip_len { diff --git a/crates/guara-core/src/service.rs b/crates/guara-core/src/service.rs index bb38e21..af297e1 100644 --- a/crates/guara-core/src/service.rs +++ b/crates/guara-core/src/service.rs @@ -225,14 +225,25 @@ impl Engine { if opts.assembly.remember_keys { opts.assembly.keys = asm.keys.clone(); } - let job = self.register(JobKind::Scan, path.to_string(), opts, Some(asm.source.clone())); + let job = self.register( + JobKind::Scan, + path.to_string(), + opts, + Some(asm.source.clone()), + ); { let mut s = job.st.lock().unwrap(); for u in &asm.unlocked { - s.warnings.push(format!("Volume cifrado aberto na posição {}: {}.", u.start, u.description)); + s.warnings.push(format!( + "Volume cifrado aberto na posição {}: {}.", + u.start, u.description + )); } for l in &asm.locked { - s.warnings.push(format!("Volume cifrado na posição {} não foi aberto: {}.", l.start, l.reason)); + s.warnings.push(format!( + "Volume cifrado na posição {} não foi aberto: {}.", + l.start, l.reason + )); } s.warnings.extend(asm.notes.iter().cloned()); } @@ -552,7 +563,12 @@ impl Engine { } /// Cria a imagem em bruto ou em outro formato forense (E01 ou AFF4). - pub fn start_image_as(&self, path: &str, dest: &str, format: &image::ImageFormat) -> Result { + pub fn start_image_as( + &self, + path: &str, + dest: &str, + format: &image::ImageFormat, + ) -> Result { let format = format.clone(); let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?; recover::check_destination( @@ -848,8 +864,14 @@ impl Engine { let mut last_err = None; for path in &tried { match open_path(Path::new(path)) { - Ok(src) if crate::reconnect::matches(src.as_ref(), if j.opts.assembly.is_plain() { size } else { 0 }, fp.as_deref()) => { - *j.src.lock().unwrap() = Some(finish_source(&j, src)); + Ok(src) + if crate::reconnect::matches( + src.as_ref(), + if j.opts.assembly.is_plain() { size } else { 0 }, + fp.as_deref(), + ) => + { + *j.src.lock().unwrap() = Some(finish_source(j, src)); *j.src_path.lock().unwrap() = path.clone(); return Ok(None); } @@ -860,7 +882,7 @@ impl Engine { if let Some((path, src)) = crate::reconnect::find_matching_source(size, fp.as_deref(), &tried) { - *j.src.lock().unwrap() = Some(finish_source(&j, src)); + *j.src.lock().unwrap() = Some(finish_source(j, src)); *j.src_path.lock().unwrap() = path.clone(); return Ok(Some(path)); } @@ -1019,10 +1041,15 @@ impl Engine { } layout.warnings.extend(a.notes.iter().cloned()); for u in &a.unlocked { - layout.warnings.push(format!("Volume aberto em {}: {}.", u.start, u.description)); + layout + .warnings + .push(format!("Volume aberto em {}: {}.", u.start, u.description)); } for l in &a.locked { - layout.warnings.push(format!("Volume cifrado em {} continua fechado: {}.", l.start, l.reason)); + layout.warnings.push(format!( + "Volume cifrado em {} continua fechado: {}.", + l.start, l.reason + )); } json!(layout) } diff --git a/crates/guara-core/src/signatures.rs b/crates/guara-core/src/signatures.rs index 5d9920f..b1a6a40 100644 --- a/crates/guara-core/src/signatures.rs +++ b/crates/guara-core/src/signatures.rs @@ -20,13 +20,19 @@ pub fn category_for_ext(ext: &str) -> Category { "jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp" | "heic" | "avif" | "tif" | "tiff" | "psd" | "svg" | "ico" | "cr2" | "nef" | "dng" | "jp2" => Category::Image, "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" | "ods" | "odp" - | "txt" | "rtf" | "epub" | "md" | "csv" | "html" | "xml" | "json" | "swf" => Category::Document, - "mp4" | "mov" | "m4v" | "avi" | "mkv" | "webm" | "wmv" | "flv" | "mpg" | "mpeg" | "3gp" | "ts" => { - Category::Video + | "txt" | "rtf" | "epub" | "md" | "csv" | "html" | "xml" | "json" | "swf" => { + Category::Document + } + "mp4" | "mov" | "m4v" | "avi" | "mkv" | "webm" | "wmv" | "flv" | "mpg" | "mpeg" | "3gp" + | "ts" => Category::Video, + "mp3" | "wav" | "flac" | "ogg" | "m4a" | "aac" | "wma" | "opus" | "mid" | "aif" => { + Category::Audio + } + "zip" | "rar" | "7z" | "gz" | "tar" | "bz2" | "xz" | "jar" | "apk" | "zst" | "cab" + | "deb" => Category::Archive, + "sqlite" | "db" | "sqlite3" | "mdb" | "accdb" | "pst" | "dat" | "evtx" => { + Category::Database } - "mp3" | "wav" | "flac" | "ogg" | "m4a" | "aac" | "wma" | "opus" | "mid" | "aif" => Category::Audio, - "zip" | "rar" | "7z" | "gz" | "tar" | "bz2" | "xz" | "jar" | "apk" | "zst" | "cab" | "deb" => Category::Archive, - "sqlite" | "db" | "sqlite3" | "mdb" | "accdb" | "pst" | "dat" | "evtx" => Category::Database, _ => Category::Other, } } @@ -85,22 +91,22 @@ pub fn matches_ext(ext: &str, head: &[u8]) -> Option { pub fn might_start(b0: u8) -> bool { crate::formats::might_start(b0) || matches!( - b0, - 0xFF | 0x89 - | b'G' - | b'B' - | b'%' - | b'P' - | 0xD0 - | b'7' - | b'S' - | b'R' - | b'I' - | b'M' - | 0x00 - | b'f' - | 0x1F - ) + b0, + 0xFF | 0x89 + | b'G' + | b'B' + | b'%' + | b'P' + | 0xD0 + | b'7' + | b'S' + | b'R' + | b'I' + | b'M' + | 0x00 + | b'f' + | 0x1F + ) } const MAX_HEADER: usize = 64 * 1024; diff --git a/crates/guara-core/src/source.rs b/crates/guara-core/src/source.rs index 2d85513..06ae13c 100644 --- a/crates/guara-core/src/source.rs +++ b/crates/guara-core/src/source.rs @@ -277,11 +277,13 @@ pub fn open_path(path: &Path) -> io::Result> { let mut head = [0u8; 96]; let got = file.read_at(0, &mut head).unwrap_or(0); if got >= 4 && head.starts_with(b"PK") && crate::aff4::is_aff4(&head[..got]) { - let a = crate::aff4::Aff4Source::open(path).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + let a = crate::aff4::Aff4Source::open(path) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; return Ok(Arc::new(a)); } if got >= 8 && crate::ewf::is_ewf_header(&head) { - let e = crate::ewf::EwfSource::open(path).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + let e = crate::ewf::EwfSource::open(path) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; return Ok(Arc::new(e)); } Ok(Arc::new(file)) diff --git a/crates/guara-core/src/types.rs b/crates/guara-core/src/types.rs index a0786ba..e1700a1 100644 --- a/crates/guara-core/src/types.rs +++ b/crates/guara-core/src/types.rs @@ -154,19 +154,30 @@ impl Packed { pub fn read(&self, src: &dyn crate::source::Source) -> std::io::Result> { use std::io::Read; let raw = crate::source::read_vec(src, self.offset, self.disk_len as usize)?; - let bad = |m: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("extensão comprimida ilegível ({m})")); + let bad = |m: &str| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("extensão comprimida ilegível ({m})"), + ) + }; let mut data = match self.codec { PackedCodec::Zlib => { let mut v = Vec::with_capacity(self.ram_len as usize); - flate2::read::ZlibDecoder::new(&raw[..]).read_to_end(&mut v).map_err(|_| bad("zlib"))?; + flate2::read::ZlibDecoder::new(&raw[..]) + .read_to_end(&mut v) + .map_err(|_| bad("zlib"))?; v } PackedCodec::Zstd => { let mut v = Vec::with_capacity(self.ram_len as usize); - ruzstd::StreamingDecoder::new(&raw[..]).map_err(|_| bad("zstd"))?.read_to_end(&mut v).map_err(|_| bad("zstd"))?; + ruzstd::StreamingDecoder::new(&raw[..]) + .map_err(|_| bad("zstd"))? + .read_to_end(&mut v) + .map_err(|_| bad("zstd"))?; v } - PackedCodec::Lzo => crate::fs::btrfs::lzo_btrfs(&raw, self.ram_len as usize).ok_or_else(|| bad("lzo"))?, + PackedCodec::Lzo => crate::fs::btrfs::lzo_btrfs(&raw, self.ram_len as usize) + .ok_or_else(|| bad("lzo"))?, }; data.resize(self.ram_len.max(data.len() as u64) as usize, 0); let s = (self.skip as usize).min(data.len()); @@ -193,14 +204,25 @@ impl Part { mod b64 { use serde::{Deserialize, Deserializer, Serializer}; const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + #[allow(clippy::ptr_arg)] pub fn serialize(b: &Vec, s: S) -> Result { let mut out = String::with_capacity(b.len().div_ceil(3) * 4); for c in b.chunks(3) { - let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32; + let n = (c[0] as u32) << 16 + | (*c.get(1).unwrap_or(&0) as u32) << 8 + | *c.get(2).unwrap_or(&0) as u32; out.push(T[(n >> 18) as usize & 63] as char); out.push(T[(n >> 12) as usize & 63] as char); - out.push(if c.len() > 1 { T[(n >> 6) as usize & 63] as char } else { '=' }); - out.push(if c.len() > 2 { T[n as usize & 63] as char } else { '=' }); + out.push(if c.len() > 1 { + T[(n >> 6) as usize & 63] as char + } else { + '=' + }); + out.push(if c.len() > 2 { + T[n as usize & 63] as char + } else { + '=' + }); } s.serialize_str(&out) } @@ -386,7 +408,16 @@ impl FsKind { pub fn has_metadata_scanner(self) -> bool { matches!( self, - FsKind::Ntfs | FsKind::Fat12 | FsKind::Fat16 | FsKind::Fat32 | FsKind::Exfat | FsKind::Ext | FsKind::HfsPlus | FsKind::Btrfs | FsKind::Xfs | FsKind::Apfs + FsKind::Ntfs + | FsKind::Fat12 + | FsKind::Fat16 + | FsKind::Fat32 + | FsKind::Exfat + | FsKind::Ext + | FsKind::HfsPlus + | FsKind::Btrfs + | FsKind::Xfs + | FsKind::Apfs ) } } diff --git a/crates/guara-core/src/volume.rs b/crates/guara-core/src/volume.rs index e1d9b35..5c079d9 100644 --- a/crates/guara-core/src/volume.rs +++ b/crates/guara-core/src/volume.rs @@ -25,8 +25,17 @@ pub struct Overlay { impl Overlay { pub fn new(base: Arc, mut regions: Vec) -> Self { regions.sort_by_key(|r| r.start); - let size = regions.iter().map(|r| r.start + r.len).max().unwrap_or(0).max(base.size()); - Self { base, regions, size } + let size = regions + .iter() + .map(|r| r.start + r.len) + .max() + .unwrap_or(0) + .max(base.size()); + Self { + base, + regions, + size, + } } pub fn regions(&self) -> &[Region] { &self.regions diff --git a/crates/guara-core/tests/aff4.rs b/crates/guara-core/tests/aff4.rs index 27a28cc..0a8a6b6 100644 --- a/crates/guara-core/tests/aff4.rs +++ b/crates/guara-core/tests/aff4.rs @@ -11,7 +11,11 @@ fn dir() -> PathBuf { #[test] fn reads_reference_images_and_stored_hash_matches() { let mut seen = 0; - for name in ["Base-Linear.aff4", "Base-Allocated.aff4", "Base-Linear-AllHashes.aff4"] { + for name in [ + "Base-Linear.aff4", + "Base-Allocated.aff4", + "Base-Linear-AllHashes.aff4", + ] { let p = dir().join(name); if !p.exists() { eprintln!("SKIP {name}"); @@ -19,7 +23,11 @@ fn reads_reference_images_and_stored_hash_matches() { } let a = Aff4Source::open(&p).unwrap_or_else(|e| panic!("{name}: {e}")); assert_eq!(a.size(), 268_435_456, "{name}"); - assert_eq!(a.verify_stream_md5().unwrap(), Some(true), "{name}: hash do fluxo"); + assert_eq!( + a.verify_stream_md5().unwrap(), + Some(true), + "{name}: hash do fluxo" + ); // setor 0 legível pelo caminho genérico let s = open_path(&p).unwrap(); let head = read_vec(s.as_ref(), 0, 4096).unwrap(); @@ -27,7 +35,12 @@ fn reads_reference_images_and_stored_hash_matches() { // leitura em vãos do mapa devolve zeros let far = read_vec(s.as_ref(), 200 << 20, 512).unwrap(); assert_eq!(far.len(), 512); - eprintln!("{name}: {} {:?}", a.info.get("Examinador").cloned().unwrap_or_default(), guara_core::partition::detect_fs_in(&read_vec(s.as_ref(), 0, 4096).unwrap()).map(|x| x.0)); + eprintln!( + "{name}: {} {:?}", + a.info.get("Examinador").cloned().unwrap_or_default(), + guara_core::partition::detect_fs_in(&read_vec(s.as_ref(), 0, 4096).unwrap()) + .map(|x| x.0) + ); seen += 1; } if seen == 0 { @@ -43,17 +56,36 @@ fn roundtrip_with_snappy_blocks_and_odd_size() { x ^= x << 13; x ^= x >> 7; x ^= x << 17; - if i % 100_000 < 60_000 { 0 } else { (x >> 24) as u8 } + if i % 100_000 < 60_000 { + 0 + } else { + (x >> 24) as u8 + } }) .collect(); data.truncate(5_000_001); let dir = tempfile::tempdir().unwrap(); let p = dir.path().join("t.aff4"); - let opts = Aff4Options { case_name: "caso".into(), examiner: "Lucas".into(), notes: "teste".into(), ..Default::default() }; - let rep = write_aff4(&MemSource(data.clone()), &p, &opts, &|| false, &mut |_, _| {}).unwrap(); + let opts = Aff4Options { + case_name: "caso".into(), + examiner: "Lucas".into(), + notes: "teste".into(), + ..Default::default() + }; + let rep = write_aff4( + &MemSource(data.clone()), + &p, + &opts, + &|| false, + &mut |_, _| {}, + ) + .unwrap(); let a = Aff4Source::open(&p).unwrap(); assert_eq!(a.size(), data.len() as u64); - assert_eq!(a.info.get("md5").map(String::as_str), Some(rep.md5.as_str())); + assert_eq!( + a.info.get("md5").map(String::as_str), + Some(rep.md5.as_str()) + ); assert_eq!(a.info.get("Examinador").map(String::as_str), Some("Lucas")); assert_eq!(a.verify_stream_md5().unwrap(), Some(true)); let got = read_vec(&a, 0, data.len()).unwrap(); diff --git a/crates/guara-core/tests/apfs.rs b/crates/guara-core/tests/apfs.rs index 9e8a468..bca0527 100644 --- a/crates/guara-core/tests/apfs.rs +++ b/crates/guara-core/tests/apfs.rs @@ -20,7 +20,10 @@ fn reads_the_live_tree_of_the_reference_image() { eprintln!("{files:#?}"); assert!(!files.is_empty()); let names: Vec<&str> = files.iter().map(|f| f.0.as_str()).collect(); - assert!(names.iter().any(|n| n.ends_with("passwords.txt")), "{names:?}"); + assert!( + names.iter().any(|n| n.ends_with("passwords.txt")), + "{names:?}" + ); } #[test] @@ -28,16 +31,31 @@ fn checksum_matches_real_blocks() { let Some(p) = img() else { return }; let d = std::fs::read(p).unwrap(); let b = &d[0..4096]; - assert_eq!(fletcher64(&b[8..]), u64::from_le_bytes(b[0..8].try_into().unwrap())); + assert_eq!( + fletcher64(&b[8..]), + u64::from_le_bytes(b[0..8].try_into().unwrap()) + ); } #[test] fn quick_scan_is_clean_on_pristine_volume() { let Some(p) = img() else { return }; init(); - let c = scan_path(&p, &ScanOptions { mode: ScanMode::Quick, ..Default::default() }); + let c = scan_path( + &p, + &ScanOptions { + mode: ScanMode::Quick, + ..Default::default() + }, + ); assert_eq!(c.layout.as_ref().unwrap().partitions[0].fs, FsKind::Apfs); - eprintln!("{:?}", c.found.iter().map(|f| (&f.name, &f.dir, f.size)).collect::>()); + eprintln!( + "{:?}", + c.found + .iter() + .map(|f| (&f.name, &f.dir, f.size)) + .collect::>() + ); } // --- manipulação bruta de nós (independente do módulo) --- @@ -65,7 +83,10 @@ fn read_node(b: &[u8]) -> (u16, Vec) { for i in 0..n { let o = 56 + toc_off + i * 8; let (ko, kl, vo, vl) = (le16(b, o), le16(b, o + 2), le16(b, o + 4), le16(b, o + 6)); - out.push((b[key_base + ko..key_base + ko + kl].to_vec(), b[val_end - vo..val_end - vo + vl].to_vec())); + out.push(( + b[key_base + ko..key_base + ko + kl].to_vec(), + b[val_end - vo..val_end - vo + vl].to_vec(), + )); } (flags, out) } @@ -131,7 +152,12 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() { c }); let vol_oid = le64(&nx, 184); - let vol_paddr = cont_entries.iter().filter(|(k, _)| le64(k, 0) == vol_oid).map(|(_, v)| le64(v, 8)).last().unwrap(); + let vol_paddr = cont_entries + .iter() + .filter(|(k, _)| le64(k, 0) == vol_oid) + .map(|(_, v)| le64(v, 8)) + .next_back() + .unwrap(); let vsb = blk(&d, vol_paddr); let (vol_omap_paddr, root_oid) = (le64(&vsb, 128), le64(&vsb, 136)); let vol_omap = blk(&d, vol_omap_paddr); @@ -140,23 +166,39 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() { let vt_orig = vt.clone(); fixed_to_var(&mut vt); let (_, omap_entries) = read_node(&vt); - let (root_idx, root_paddr) = omap_entries.iter().enumerate().filter(|(_, (k, _))| le64(k, 0) == root_oid).map(|(i, (_, v))| (i, le64(v, 8))).last().unwrap(); + let (root_idx, root_paddr) = omap_entries + .iter() + .enumerate() + .filter(|(_, (k, _))| le64(k, 0) == root_oid) + .map(|(i, (_, v))| (i, le64(v, 8))) + .next_back() + .unwrap(); let root = blk(&d, root_paddr); assert_eq!(le16(&root, 34), 0, "raiz deveria ser folha nesta imagem"); let (_, recs) = read_node(&root); // inode do arquivo let victim = b"passwords.txt"; - let ino = recs.iter().find_map(|(k, v)| { - let hdr = le64(k, 0); - ((hdr >> 60) == 9 && k.len() > 12 && k[12..].starts_with(victim)).then(|| le64(v, 0)) - }).expect("entrada de diretório"); - let private_id = recs.iter().find_map(|(k, v)| ((le64(k, 0) >> 60) == 3 && (le64(k, 0) & 0x0FFF_FFFF_FFFF_FFFF) == ino).then(|| le64(v, 8))).unwrap(); + let ino = recs + .iter() + .find_map(|(k, v)| { + let hdr = le64(k, 0); + ((hdr >> 60) == 9 && k.len() > 12 && k[12..].starts_with(victim)).then(|| le64(v, 0)) + }) + .expect("entrada de diretório"); + let private_id = recs + .iter() + .find_map(|(k, v)| { + ((le64(k, 0) >> 60) == 3 && (le64(k, 0) & 0x0FFF_FFFF_FFFF_FFFF) == ino) + .then(|| le64(v, 8)) + }) + .unwrap(); let kept: Vec = recs .iter() .filter(|(k, v)| { let hdr = le64(k, 0); let (id, ty) = (hdr & 0x0FFF_FFFF_FFFF_FFFF, hdr >> 60); - let drec_of_victim = ty == 9 && k.len() > 12 && k[12..].starts_with(victim) && le64(v, 0) == ino; + let drec_of_victim = + ty == 9 && k.len() > 12 && k[12..].starts_with(victim) && le64(v, 0) == ino; !(id == ino && ty != 9 || id == private_id && ty == 8 || drec_of_victim) }) .cloned() @@ -164,7 +206,9 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() { assert!(kept.len() < recs.len()); let newnode = write_node(&root, &kept); // bloco livre para a nova versão - let free = (10..d.len() / bs).find(|&n| d[n * bs..(n + 1) * bs].iter().all(|&x| x == 0) && n != vol_tree_paddr as usize).expect("bloco livre"); + let free = (10..d.len() / bs) + .find(|&n| d[n * bs..(n + 1) * bs].iter().all(|&x| x == 0) && n != vol_tree_paddr as usize) + .expect("bloco livre"); d[free * bs..(free + 1) * bs].copy_from_slice(&newnode); // aponta o mapa de objetos para a nova versão (mesmo xid), editando o valor da entrada let toc_off = le16(&vt_orig, 40); @@ -185,8 +229,26 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() { let src = FileSource::open(&p2).unwrap(); assert!(!live_paths(&src, 0).iter().any(|f| f.0 == "passwords.txt")); init(); - let c = scan_path(&p2, &ScanOptions { mode: ScanMode::Deep, ..Default::default() }); - let f = c.found.iter().find(|f| f.origin == Origin::Apfs && f.name == "passwords.txt").unwrap_or_else(|| panic!("{:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::>())); + let c = scan_path( + &p2, + &ScanOptions { + mode: ScanMode::Deep, + ..Default::default() + }, + ); + let f = c + .found + .iter() + .find(|f| f.origin == Origin::Apfs && f.name == "passwords.txt") + .unwrap_or_else(|| { + panic!( + "{:?}", + c.found + .iter() + .map(|f| (&f.name, f.origin)) + .collect::>() + ) + }); assert_eq!(f.size, 116); let out = dir.path().join("out"); let rep = recover_all(&p2, std::slice::from_ref(f), &out); @@ -194,7 +256,12 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() { let rp = PathBuf::from(&rep.recovered[0].path); let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap(); assert_eq!(got.len(), 116); - assert!(String::from_utf8_lossy(&got).chars().all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t'), "{got:?}"); + assert!( + String::from_utf8_lossy(&got) + .chars() + .all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t'), + "{got:?}" + ); } /// Converte a tabela de entradas fixas (4 bytes) de um nó de omap para o formato variável (8 bytes) em memória. @@ -212,7 +279,10 @@ fn fixed_to_var(b: &mut Vec) { for i in 0..n { let o = 56 + toc_off + i * 4; let (ko, vo) = (le16(b, o), le16(b, o + 2)); - entries.push((b[key_base + ko..key_base + ko + 16].to_vec(), b[val_end - vo..val_end - vo + 16].to_vec())); + entries.push(( + b[key_base + ko..key_base + ko + 16].to_vec(), + b[val_end - vo..val_end - vo + 16].to_vec(), + )); } let mut nb = b.clone(); nb[32..34].copy_from_slice(&(flags & !4).to_le_bytes()); diff --git a/crates/guara-core/tests/bitlocker.rs b/crates/guara-core/tests/bitlocker.rs index 05ddb8f..421caca 100644 --- a/crates/guara-core/tests/bitlocker.rs +++ b/crates/guara-core/tests/bitlocker.rs @@ -13,10 +13,13 @@ fn dir() -> Option { d.join("images.conf").exists().then_some(d) } -fn conf(d: &PathBuf) -> BTreeMap> { +fn conf(d: &std::path::Path) -> BTreeMap> { let mut m: BTreeMap> = BTreeMap::new(); let mut cur = String::new(); - for l in std::fs::read_to_string(d.join("images.conf")).unwrap().lines() { + for l in std::fs::read_to_string(d.join("images.conf")) + .unwrap() + .lines() + { let l = l.trim(); if let Some(n) = l.strip_prefix('[').and_then(|x| x.strip_suffix(']')) { cur = n.to_string(); @@ -34,7 +37,11 @@ fn check_fs(src: &dyn Source, what: &str) { } let head = read_vec(src, 0, 4096).unwrap(); let fs = detect_fs_in(&head); - assert!(fs.is_some(), "{what}: setor 0 aberto não é um sistema de arquivos: {:02x?}", &head[..32]); + assert!( + fs.is_some(), + "{what}: setor 0 aberto não é um sistema de arquivos: {:02x?}", + &head[..32] + ); } #[test] @@ -66,19 +73,23 @@ fn opens_all_reference_images_with_password_and_recovery_key() { } let mut views = vec![]; if !pw.is_empty() { - let u = bitlocker::unlock(src.clone(), &[Secret::Password(pw.clone())]).unwrap_or_else(|e| panic!("{name} senha: {e}")); + let u = bitlocker::unlock(src.clone(), &[Secret::Password(pw.clone())]) + .unwrap_or_else(|e| panic!("{name} senha: {e}")); check_fs(u.source.as_ref(), &format!("{name} (senha)")); views.push(u); } if !rp.is_empty() { - let u = bitlocker::unlock(src.clone(), &[Secret::RecoveryKey(rp.clone())]).unwrap_or_else(|e| panic!("{name} recuperação: {e}")); + let u = bitlocker::unlock(src.clone(), &[Secret::RecoveryKey(rp.clone())]) + .unwrap_or_else(|e| panic!("{name} recuperação: {e}")); check_fs(u.source.as_ref(), &format!("{name} (recuperação)")); views.push(u); } if let Some(g) = c.get("SK_VMK_GUID") { let f = d.join(format!("{}.BEK", g.to_uppercase())); if f.exists() { - let u = bitlocker::unlock(src.clone(), &[Secret::KeyFile(std::fs::read(f).unwrap())]).unwrap_or_else(|e| panic!("{name} BEK: {e}")); + let u = + bitlocker::unlock(src.clone(), &[Secret::KeyFile(std::fs::read(f).unwrap())]) + .unwrap_or_else(|e| panic!("{name} BEK: {e}")); check_fs(u.source.as_ref(), &format!("{name} (BEK)")); views.push(u); } @@ -86,10 +97,17 @@ fn opens_all_reference_images_with_password_and_recovery_key() { assert!(!views.is_empty(), "{name}: nenhum protetor testável"); let first = read_vec(views[0].source.as_ref(), 0, 1 << 20).unwrap(); for v in &views[1..] { - assert!(read_vec(v.source.as_ref(), 0, 1 << 20).unwrap() == first, "{name}: protetores divergem"); + assert!( + read_vec(v.source.as_ref(), 0, 1 << 20).unwrap() == first, + "{name}: protetores divergem" + ); } opened += 1; - eprintln!("ok {name}: {} ({} protetores)", views[0].description, views.len()); + eprintln!( + "ok {name}: {} ({} protetores)", + views[0].description, + views.len() + ); } assert!(opened > 0); } diff --git a/crates/guara-core/tests/btrfs.rs b/crates/guara-core/tests/btrfs.rs index 5690447..46814b6 100644 --- a/crates/guara-core/tests/btrfs.rs +++ b/crates/guara-core/tests/btrfs.rs @@ -4,13 +4,17 @@ mod common; use common::*; use guara_core::fs::btrfs::{crc32c, live_paths, lzo1x_decompress}; -use guara_core::source::{FileSource, Source}; +use guara_core::source::FileSource; use guara_core::*; use std::path::{Path, PathBuf}; use std::process::Command; fn have(t: &str) -> bool { - Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false) + Command::new("sh") + .args(["-c", &format!("command -v {t}")]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) } fn data(seed: u64, n: usize) -> Vec { @@ -51,10 +55,22 @@ fn build() -> Option { std::fs::write(root.join(p), c).unwrap(); } let path = dir.path().join("b.img"); - std::fs::File::create(&path).unwrap().set_len(128 << 20).unwrap(); - let o = Command::new("mkfs.btrfs").args(["-q", "-f", "--rootdir"]).arg(&root).arg(&path).output().unwrap(); + std::fs::File::create(&path) + .unwrap() + .set_len(128 << 20) + .unwrap(); + let o = Command::new("mkfs.btrfs") + .args(["-q", "-f", "--rootdir"]) + .arg(&root) + .arg(&path) + .output() + .unwrap(); assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr)); - Some(Img { _dir: dir, path, files }) + Some(Img { + _dir: dir, + path, + files, + }) } #[test] @@ -63,7 +79,10 @@ fn lists_the_live_tree_with_paths_and_sizes() { let src = FileSource::open(&i.path).unwrap(); let listed = live_paths(&src, 0); for (p, c) in &i.files { - let f = listed.iter().find(|x| x.0 == *p).unwrap_or_else(|| panic!("{p} ausente: {listed:?}")); + let f = listed + .iter() + .find(|x| x.0 == *p) + .unwrap_or_else(|| panic!("{p} ausente: {listed:?}")); assert_eq!(f.1, c.len() as u64, "{p}"); assert!(!f.2); } @@ -164,10 +183,16 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() { let root_leaf = (t.root_tree as usize, t.root_tree_logical); let ext_leaf = (t.extent_tree as usize, t.extent_tree_logical); let to_logical = |phys: usize| -> u64 { - let c = t.chunks.iter().find(|c| (phys as u64) >= c.2 && (phys as u64) < c.2 + c.1).expect("chunk"); + let c = t + .chunks + .iter() + .find(|c| (phys as u64) >= c.2 && (phys as u64) < c.2 + c.1) + .expect("chunk"); c.0 + (phys as u64 - c.2) }; - let fl = Leaf { raw: img[fs_leaf.0..fs_leaf.0 + nodesize].to_vec() }; + let fl = Leaf { + raw: img[fs_leaf.0..fs_leaf.0 + nodesize].to_vec(), + }; // inode do arquivo apagado let victim = "relatorio.dat"; let mut ino = 0; @@ -194,14 +219,23 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() { // Novo bloco livre para a nova versão: primeiro bloco do metadata sem folha válida e todo zero. let free_slot = (0..img.len() / nodesize) .map(|n| n * nodesize) - .find(|&o| o > fs_leaf.0 && img[o..o + nodesize].iter().all(|&x| x == 0) && t.chunks.iter().any(|c| (o as u64) >= c.2 && (o as u64) < c.2 + c.1) && o < fs_leaf.0 + (8 << 20)) + .find(|&o| { + o > fs_leaf.0 + && img[o..o + nodesize].iter().all(|&x| x == 0) + && t.chunks + .iter() + .any(|c| (o as u64) >= c.2 && (o as u64) < c.2 + c.1) + && o < fs_leaf.0 + (8 << 20) + }) .expect("bloco livre"); // logical = bytenr da folha antiga + (offset físico - offset físico antigo): mesmo chunk let new_bytenr = to_logical(free_slot); let newleaf = fl.without(|o, _| o == ino, new_bytenr); img[free_slot..free_slot + nodesize].copy_from_slice(&newleaf.raw); // Aponta o ROOT_ITEM da árvore 5 para a nova versão (e sobe a geração), na folha da árvore raiz. - let mut rl = Leaf { raw: img[root_leaf.0..root_leaf.0 + nodesize].to_vec() }; + let mut rl = Leaf { + raw: img[root_leaf.0..root_leaf.0 + nodesize].to_vec(), + }; for k in 0..rl.nritems() { let (obj, ty, _, off, _) = rl.item(k); if obj == 5 && ty == 132 { @@ -212,7 +246,9 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() { rl.seal(); img[root_leaf.0..root_leaf.0 + nodesize].copy_from_slice(&rl.raw); // Libera o extent de dados no extent tree (em outro endereço da mesma folha). - let el = Leaf { raw: img[ext_leaf.0..ext_leaf.0 + nodesize].to_vec() }; + let el = Leaf { + raw: img[ext_leaf.0..ext_leaf.0 + nodesize].to_vec(), + }; let newext = el.without(|o, ty| o == data_bytenr && ty == 168, ext_leaf.1); let _ = to_logical; img[ext_leaf.0..ext_leaf.0 + nodesize].copy_from_slice(&newext.raw); @@ -221,15 +257,33 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() { let p2 = i._dir.path().join("deleted.img"); std::fs::write(&p2, &img).unwrap(); init(); - let c = scan_path(&p2, &ScanOptions { mode: ScanMode::Deep, ..Default::default() }); + let c = scan_path( + &p2, + &ScanOptions { + mode: ScanMode::Deep, + ..Default::default() + }, + ); let f = c .found .iter() .find(|f| f.name == victim && f.origin == Origin::Btrfs) - .unwrap_or_else(|| panic!("não achou: {:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::>())); + .unwrap_or_else(|| { + panic!( + "não achou: {:?}", + c.found + .iter() + .map(|f| (&f.name, f.origin)) + .collect::>() + ) + }); assert_eq!(f.size, 150_000); assert_eq!(f.dir.as_deref(), Some("/docs")); - assert!(f.confidence >= Confidence::Medium && f.note.as_deref().unwrap_or("").contains("livres"), "{:?}", f.note); + assert!( + f.confidence >= Confidence::Medium && f.note.as_deref().unwrap_or("").contains("livres"), + "{:?}", + f.note + ); let out = i._dir.path().join("out"); let rep = recover_all(&p2, std::slice::from_ref(f), &out); assert!(rep.failed.is_empty(), "{:?}", rep.failed); @@ -237,7 +291,10 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() { let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap(); assert!(got == i.files[0].1); // Os arquivos que continuam existindo não aparecem como apagados. - assert!(!c.found.iter().any(|f| f.name == "fica.dat" && f.origin == Origin::Btrfs)); + assert!(!c + .found + .iter() + .any(|f| f.name == "fica.dat" && f.origin == Origin::Btrfs)); let _ = Path::new(""); } @@ -247,7 +304,10 @@ fn lzo_literals_and_a_match() { let mut s = vec![17 + 20]; s.extend((0..20u8).map(|i| b'a' + i)); s.extend_from_slice(&[0x11, 0, 0]); - assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), (0..20u8).map(|i| b'a' + i).collect::>()); + assert_eq!( + lzo1x_decompress(&s, 4096).unwrap(), + (0..20u8).map(|i| b'a' + i).collect::>() + ); // "abcd" + M2 (dist 4, len 4) + fim let s = vec![17 + 4, b'a', b'b', b'c', b'd', 0x6C, 0x00, 0x11, 0, 0]; assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), b"abcdabcd"); @@ -265,16 +325,35 @@ fn packed_extents_decode_zlib_and_zstd() { let z = e.finish().unwrap(); let mut disk = vec![0u8; 4096]; disk.extend_from_slice(&z); - let p = Packed { offset: 4096, disk_len: z.len() as u64, ram_len: plain.len() as u64, skip: 1000, len: 5000, codec: PackedCodec::Zlib }; + let p = Packed { + offset: 4096, + disk_len: z.len() as u64, + ram_len: plain.len() as u64, + skip: 1000, + len: 5000, + codec: PackedCodec::Zlib, + }; assert_eq!(p.read(&MemSource(disk)).unwrap(), &plain[1000..6000]); // zstd, se o utilitário existir if have("zstd") { let dir = tempfile::tempdir().unwrap(); let f = dir.path().join("p.bin"); std::fs::write(&f, &plain).unwrap(); - assert!(Command::new("zstd").args(["-q", "-f"]).arg(&f).status().unwrap().success()); + assert!(Command::new("zstd") + .args(["-q", "-f"]) + .arg(&f) + .status() + .unwrap() + .success()); let z = std::fs::read(dir.path().join("p.bin.zst")).unwrap(); - let p = Packed { offset: 0, disk_len: z.len() as u64, ram_len: plain.len() as u64, skip: 0, len: plain.len() as u64, codec: PackedCodec::Zstd }; + let p = Packed { + offset: 0, + disk_len: z.len() as u64, + ram_len: plain.len() as u64, + skip: 0, + len: plain.len() as u64, + codec: PackedCodec::Zstd, + }; assert!(p.read(&MemSource(z)).unwrap() == plain); } } diff --git a/crates/guara-core/tests/defrag.rs b/crates/guara-core/tests/defrag.rs index f686b98..1a40a4a 100644 --- a/crates/guara-core/tests/defrag.rs +++ b/crates/guara-core/tests/defrag.rs @@ -17,15 +17,23 @@ fn photo_n(w: u16, h: u16, seed: u64, shift: i32, nmask: u64) -> Vec { let noise = ((x >> 40) & nmask) as i32; let fx = xx as f32 / w as f32; let fy = yy as f32 / h as f32; - let wave = ((fx * 9.0 + seed as f32 * 0.37).sin() * (fy * 7.0 + 1.3).cos() * 40.0) as i32 + ((fx * 31.0).sin() * 8.0) as i32; + let wave = ((fx * 9.0 + seed as f32 * 0.37).sin() * (fy * 7.0 + 1.3).cos() * 40.0) + as i32 + + ((fx * 31.0).sin() * 8.0) as i32; let base = 110 + shift + wave + (xx * 40 / w as i32); - let blob = if ((xx - 300).pow(2) + (yy - 200).pow(2)) < 5000 { 60 } else { 0 }; + let blob = if ((xx - 300).pow(2) + (yy - 200).pow(2)) < 5000 { + 60 + } else { + 0 + }; let v = (base + blob + noise).clamp(0, 255) as u8; rgb.extend_from_slice(&[v, v.saturating_sub(10), (v as i32 / 2 + noise) as u8]); } } let mut out = vec![]; - jpeg_encoder::Encoder::new(&mut out, 92).encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb).unwrap(); + jpeg_encoder::Encoder::new(&mut out, 92) + .encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb) + .unwrap(); out } @@ -71,7 +79,10 @@ fn rebuilds_a_jpeg_split_around_another_one() { // O fragmento correto precisa estar marcado. assert!(idx.get(a2_off)); eprintln!("TRUE a2_off={a2_off}"); - let pool = Pool { index: &idx, used: &[Extent::new(b_off, bb.len() as u64)] }; + let pool = Pool { + index: &idx, + used: &[Extent::new(b_off, bb.len() as u64)], + }; match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) { Analysis::Rebuilt { extents, size } => { assert_eq!(size as usize, a.len(), "tamanho"); @@ -79,13 +90,18 @@ fn rebuilds_a_jpeg_split_around_another_one() { assert_eq!(extents[0], Extent::new(a_off, split as u64)); assert_eq!(extents[1].offset, a2_off); let mut got = disk[a_off as usize..a_off as usize + split].to_vec(); - got.extend_from_slice(&disk[a2_off as usize..a2_off as usize + (size as usize - split)]); + got.extend_from_slice( + &disk[a2_off as usize..a2_off as usize + (size as usize - split)], + ); assert!(got == a, "conteúdo recomposto difere"); } other => panic!("esperava Rebuilt, veio {other:?}"), } // O outro JPEG, íntegro, continua íntegro. - let pool2 = Pool { index: &idx, used: &[] }; + let pool2 = Pool { + index: &idx, + used: &[], + }; match analyze_jpeg(&src, b_off, 4 << 20, &pool2, true) { Analysis::Intact { len } => assert_eq!(len as usize, b.len()), other => panic!("B deveria estar íntegro: {other:?}"), @@ -129,7 +145,10 @@ fn rebuilds_three_fragments_among_many_decoys() { .filter(|(i, _)| *i != 2) // o fragmento do meio de A não é de outro arquivo .map(|(_, (o, l))| Extent::new(*o, *l)) .collect(); - let pool = Pool { index: &idx, used: &used }; + let pool = Pool { + index: &idx, + used: &used, + }; match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) { Analysis::Rebuilt { extents, size } => { assert_eq!(size as usize, n); @@ -167,17 +186,36 @@ fn deep_scan_recovers_fragmented_jpeg_end_to_end() { let dir = tempfile::tempdir().unwrap(); let img = dir.path().join("d.img"); std::fs::write(&img, &disk).unwrap(); - let c = common::scan_path(&img, &ScanOptions { mode: ScanMode::Deep, ..Default::default() }); + let c = common::scan_path( + &img, + &ScanOptions { + mode: ScanMode::Deep, + ..Default::default() + }, + ); let jpgs: Vec<_> = c.found.iter().filter(|f| f.ext == "jpg").collect(); - assert_eq!(jpgs.len(), 2, "{:?}", jpgs.iter().map(|f| (f.size, f.first_offset(), f.confidence)).collect::>()); - let fa = jpgs.iter().find(|f| f.first_offset() == Some(a_off as u64)).unwrap(); + assert_eq!( + jpgs.len(), + 2, + "{:?}", + jpgs.iter() + .map(|f| (f.size, f.first_offset(), f.confidence)) + .collect::>() + ); + let fa = jpgs + .iter() + .find(|f| f.first_offset() == Some(a_off as u64)) + .unwrap(); assert_eq!(fa.size as usize, n, "{:?}", fa.note); let out = dir.path().join("out"); let rep = common::recover_all(&img, std::slice::from_ref(*fa), &out); let p = std::path::PathBuf::from(&rep.recovered[0].path); let got = std::fs::read(if p.is_absolute() { p } else { out.join(p) }).unwrap(); assert!(got == a, "JPEG recuperado difere do original"); - let fb = jpgs.iter().find(|f| f.first_offset() != Some(a_off as u64)).unwrap(); + let fb = jpgs + .iter() + .find(|f| f.first_offset() != Some(a_off as u64)) + .unwrap(); assert_eq!(fb.size as usize, photo_n(1600, 1200, 77, 30, 3).len()); let _ = (b_len, recover::RecoverReport::default()); } diff --git a/crates/guara-core/tests/e2e_crypt.rs b/crates/guara-core/tests/e2e_crypt.rs index 8dae126..266ebfb 100644 --- a/crates/guara-core/tests/e2e_crypt.rs +++ b/crates/guara-core/tests/e2e_crypt.rs @@ -11,13 +11,26 @@ use std::process::{Command, Stdio}; use std::sync::Arc; fn have(t: &str) -> bool { - Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false) + Command::new("sh") + .args(["-c", &format!("command -v {t}")]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) } fn run_stdin(c: &mut Command, input: &str) -> bool { use std::io::Write; - let mut ch = c.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn().unwrap(); - ch.stdin.take().unwrap().write_all(input.as_bytes()).unwrap(); + let mut ch = c + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + ch.stdin + .take() + .unwrap() + .write_all(input.as_bytes()) + .unwrap(); ch.wait().unwrap().success() } @@ -47,30 +60,94 @@ fn deleted_file_inside_luks2_is_recovered_with_password() { std::fs::write(root.join("docs/segredo.dat"), &secret_doc).unwrap(); std::fs::write(root.join("docs/fica.dat"), data(12, 5000)).unwrap(); let plain = dir.path().join("plain.img"); - std::fs::File::create(&plain).unwrap().set_len(8 << 20).unwrap(); - assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success()); + std::fs::File::create(&plain) + .unwrap() + .set_len(8 << 20) + .unwrap(); + assert!(Command::new("mke2fs") + .args(["-q", "-F", "-t", "ext4", "-d"]) + .arg(&root) + .arg(&plain) + .status() + .unwrap() + .success()); let dbg = |c: &str| { - Command::new("debugfs").arg("-w").arg("-R").arg(c).arg(&plain).output().unwrap() + Command::new("debugfs") + .arg("-w") + .arg("-R") + .arg(c) + .arg(&plain) + .output() + .unwrap() }; dbg("rm /docs/segredo.dat"); let img = dir.path().join("luks.img"); - std::fs::File::create(&img).unwrap().set_len(32 << 20).unwrap(); + std::fs::File::create(&img) + .unwrap() + .set_len(32 << 20) + .unwrap(); let pw = "correct horse"; let mut fmt = Command::new("cryptsetup"); - fmt.args(["luksFormat", "--batch-mode", "--type", "luks2", "--pbkdf", "pbkdf2", "--pbkdf-force-iterations", "1000", "--sector-size", "512", "--key-file", "-"]).arg(&img); + fmt.args([ + "luksFormat", + "--batch-mode", + "--type", + "luks2", + "--pbkdf", + "pbkdf2", + "--pbkdf-force-iterations", + "1000", + "--sector-size", + "512", + "--key-file", + "-", + ]) + .arg(&img); assert!(run_stdin(&mut fmt, pw)); let vkf = dir.path().join("vk"); let mut d = Command::new("cryptsetup"); - d.args(["luksDump", "--dump-volume-key", "--batch-mode", "--key-file", "-", "--volume-key-file"]).arg(&vkf).arg(&img); + d.args([ + "luksDump", + "--dump-volume-key", + "--batch-mode", + "--key-file", + "-", + "--volume-key-file", + ]) + .arg(&vkf) + .arg(&img); assert!(run_stdin(&mut d, pw)); - let out = Command::new("cryptsetup").arg("luksDump").arg(&img).output().unwrap(); + let out = Command::new("cryptsetup") + .arg("luksDump") + .arg(&img) + .output() + .unwrap(); let t = String::from_utf8_lossy(&out.stdout).to_string(); - let off: u64 = t.lines().skip_while(|l| !l.starts_with("Data segments")).find(|l| l.contains("offset:")).unwrap().split(':').nth(1).unwrap().split_whitespace().next().unwrap().parse().unwrap(); + let off: u64 = t + .lines() + .skip_while(|l| !l.starts_with("Data segments")) + .find(|l| l.contains("offset:")) + .unwrap() + .split(':') + .nth(1) + .unwrap() + .split_whitespace() + .next() + .unwrap() + .parse() + .unwrap(); assert!(Command::new("python3") .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/support/luks_encrypt.py")) - .arg(&img).arg(&plain).arg(&vkf).arg(off.to_string()).arg("aes-xts-plain64").arg("512") - .status().unwrap().success()); + .arg(&img) + .arg(&plain) + .arg(&vkf) + .arg(off.to_string()) + .arg("aes-xts-plain64") + .arg("512") + .status() + .unwrap() + .success()); let base: Arc = Arc::new(FileSource::open(&img).unwrap()); // Sem senha: o volume segue fechado e a varredura não inventa arquivos. @@ -78,19 +155,52 @@ fn deleted_file_inside_luks2_is_recovered_with_password() { assert_eq!(locked.unlocked.len(), 0); assert_eq!(locked.locked.len(), 1); - let asm = Assembly { secrets: vec![SecretInput::Password { value: pw.into() }], ..Default::default() }; + let asm = Assembly { + secrets: vec![SecretInput::Password { value: pw.into() }], + ..Default::default() + }; let open = assemble_source(base, &asm).unwrap(); assert_eq!(open.unlocked.len(), 1); let mut sink = Collect::default(); - let outc = scan::run(open.source.as_ref(), &ScanOptions { mode: ScanMode::Deep, ..Default::default() }, None, &[], &|| false, &mut sink); + let outc = scan::run( + open.source.as_ref(), + &ScanOptions { + mode: ScanMode::Deep, + ..Default::default() + }, + None, + &[], + &|| false, + &mut sink, + ); assert!(matches!(outc, Outcome::Completed)); - let f = sink.found.iter().find(|f| f.name == "segredo.dat").unwrap_or_else(|| panic!("não achou: {:?}", sink.found.iter().map(|f| &f.name).collect::>())); + let f = sink + .found + .iter() + .find(|f| f.name == "segredo.dat") + .unwrap_or_else(|| { + panic!( + "não achou: {:?}", + sink.found.iter().map(|f| &f.name).collect::>() + ) + }); assert_eq!(f.size, 120_000); let dest = tempfile::tempdir().unwrap(); let refs = vec![f]; - let rep = recover::recover(open.source.as_ref(), &refs, dest.path(), &|| false, &mut |_, _, _| {}).unwrap(); + let rep = recover::recover( + open.source.as_ref(), + &refs, + dest.path(), + &|| false, + &mut |_, _, _| {}, + ) + .unwrap(); assert!(rep.failed.is_empty()); let p = &rep.recovered[0].path; - let p = if Path::new(p).is_absolute() { Path::new(p).to_path_buf() } else { dest.path().join(p) }; + let p = if Path::new(p).is_absolute() { + Path::new(p).to_path_buf() + } else { + dest.path().join(p) + }; assert!(std::fs::read(p).unwrap() == secret_doc); } diff --git a/crates/guara-core/tests/e2e_raid.rs b/crates/guara-core/tests/e2e_raid.rs index 04ff4aa..c4d17da 100644 --- a/crates/guara-core/tests/e2e_raid.rs +++ b/crates/guara-core/tests/e2e_raid.rs @@ -11,7 +11,11 @@ use std::process::Command; use std::sync::Arc; fn have(t: &str) -> bool { - Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false) + Command::new("sh") + .args(["-c", &format!("command -v {t}")]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) } fn data(seed: u64, n: usize) -> Vec { @@ -67,9 +71,24 @@ fn deleted_file_on_degraded_raid5() { let dd = n - 1; let rows = 40usize; let logical_len = rows * dd * CHUNK; - std::fs::File::create(&plain).unwrap().set_len(logical_len as u64).unwrap(); - assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success()); - Command::new("debugfs").arg("-w").arg("-R").arg("rm /fotos/ferias.dat").arg(&plain).output().unwrap(); + std::fs::File::create(&plain) + .unwrap() + .set_len(logical_len as u64) + .unwrap(); + assert!(Command::new("mke2fs") + .args(["-q", "-F", "-t", "ext4", "-d"]) + .arg(&root) + .arg(&plain) + .status() + .unwrap() + .success()); + Command::new("debugfs") + .arg("-w") + .arg("-R") + .arg("rm /fotos/ferias.dat") + .arg(&plain) + .output() + .unwrap(); let logical = std::fs::read(&plain).unwrap(); // RAID 5 left-symmetric @@ -101,24 +120,56 @@ fn deleted_file_on_degraded_raid5() { } let base: Arc = Arc::new(FileSource::open(&paths[0]).unwrap()); let asm = Assembly { - members: paths[1..].iter().map(|p| p.to_string_lossy().to_string()).collect(), + members: paths[1..] + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(), ..Default::default() }; let a = assemble_source(base, &asm).unwrap(); - assert!(a.notes.iter().any(|n| n.contains("degradado")), "{:?}", a.notes); + assert!( + a.notes.iter().any(|n| n.contains("degradado")), + "{:?}", + a.notes + ); assert_eq!(a.extra_parts.len(), 1); assert_eq!(a.extra_parts[0].fs, FsKind::Ext); - let opts = ScanOptions { mode: ScanMode::Deep, extra_parts: a.extra_parts.clone(), covered: a.covered.clone(), ..Default::default() }; + let opts = ScanOptions { + mode: ScanMode::Deep, + extra_parts: a.extra_parts.clone(), + covered: a.covered.clone(), + ..Default::default() + }; let mut sink = Collect::default(); let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink); assert!(matches!(out, Outcome::Completed)); - let f = sink.found.iter().find(|f| f.name == "ferias.dat").unwrap_or_else(|| panic!("{:?}", sink.found.iter().map(|f| &f.name).collect::>())); + let f = sink + .found + .iter() + .find(|f| f.name == "ferias.dat") + .unwrap_or_else(|| { + panic!( + "{:?}", + sink.found.iter().map(|f| &f.name).collect::>() + ) + }); assert_eq!(f.size, 300_000); let dest = tempfile::tempdir().unwrap(); - let rep = recover::recover(a.source.as_ref(), &[f], dest.path(), &|| false, &mut |_, _, _| {}).unwrap(); + let rep = recover::recover( + a.source.as_ref(), + &[f], + dest.path(), + &|| false, + &mut |_, _, _| {}, + ) + .unwrap(); assert!(rep.failed.is_empty(), "{:?}", rep.failed); let p = &rep.recovered[0].path; - let p = if Path::new(p).is_absolute() { Path::new(p).to_path_buf() } else { dest.path().join(p) }; + let p = if Path::new(p).is_absolute() { + Path::new(p).to_path_buf() + } else { + dest.path().join(p) + }; assert!(std::fs::read(p).unwrap() == doc); } @@ -133,7 +184,11 @@ fn raid5_without_metadata_is_detected_and_scanned() { for d in 0..10 { std::fs::create_dir_all(root.join(format!("d{d}"))).unwrap(); for f in 0..30 { - std::fs::write(root.join(format!("d{d}/f{f}.txt")), vec![b'x'; 500 + f * 50]).unwrap(); + std::fs::write( + root.join(format!("d{d}/f{f}.txt")), + vec![b'x'; 500 + f * 50], + ) + .unwrap(); } } let doc = data(9, 200_000); @@ -142,9 +197,24 @@ fn raid5_without_metadata_is_detected_and_scanned() { let (n, chunk) = (3usize, 64 * 1024usize); let dd = n - 1; let rows = 160usize; - std::fs::File::create(&plain).unwrap().set_len((rows * dd * chunk) as u64).unwrap(); - assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success()); - Command::new("debugfs").arg("-w").arg("-R").arg("rm /d3/perdido.dat").arg(&plain).output().unwrap(); + std::fs::File::create(&plain) + .unwrap() + .set_len((rows * dd * chunk) as u64) + .unwrap(); + assert!(Command::new("mke2fs") + .args(["-q", "-F", "-t", "ext4", "-d"]) + .arg(&root) + .arg(&plain) + .status() + .unwrap() + .success()); + Command::new("debugfs") + .arg("-w") + .arg("-R") + .arg("rm /d3/perdido.dat") + .arg(&plain) + .output() + .unwrap(); let logical = std::fs::read(&plain).unwrap(); // left-asymmetric let mut disks = vec![vec![0u8; rows * chunk]; n]; @@ -169,21 +239,48 @@ fn raid5_without_metadata_is_detected_and_scanned() { } let base: Arc = Arc::new(FileSource::open(&paths[0]).unwrap()); let asm = Assembly { - members: paths[1..].iter().map(|p| p.to_string_lossy().to_string()).collect(), + members: paths[1..] + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(), raid: Some(guara_core::assemble::RaidSpec::default()), ..Default::default() }; let a = assemble_source(base, &asm).unwrap(); assert!(a.skip_primary); - assert!(a.notes.iter().any(|n| n.contains("descobertos")), "{:?}", a.notes); - let opts = ScanOptions { mode: ScanMode::Deep, extra_parts: a.extra_parts.clone(), skip_primary: true, ..Default::default() }; + assert!( + a.notes.iter().any(|n| n.contains("descobertos")), + "{:?}", + a.notes + ); + let opts = ScanOptions { + mode: ScanMode::Deep, + extra_parts: a.extra_parts.clone(), + skip_primary: true, + ..Default::default() + }; let mut sink = Collect::default(); let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink); assert!(matches!(out, Outcome::Completed)); - let f = sink.found.iter().find(|f| f.name == "perdido.dat").expect("perdido.dat"); + let f = sink + .found + .iter() + .find(|f| f.name == "perdido.dat") + .expect("perdido.dat"); let dest = tempfile::tempdir().unwrap(); - let rep = recover::recover(a.source.as_ref(), &[f], dest.path(), &|| false, &mut |_, _, _| {}).unwrap(); + let rep = recover::recover( + a.source.as_ref(), + &[f], + dest.path(), + &|| false, + &mut |_, _, _| {}, + ) + .unwrap(); let p = &rep.recovered[0].path; - let p = if Path::new(p).is_absolute() { Path::new(p).to_path_buf() } else { dest.path().join(p) }; + let p = if Path::new(p).is_absolute() { + Path::new(p).to_path_buf() + } else { + dest.path().join(p) + }; assert!(std::fs::read(p).unwrap() == doc); } diff --git a/crates/guara-core/tests/ewf.rs b/crates/guara-core/tests/ewf.rs index 711f089..6a47f59 100644 --- a/crates/guara-core/tests/ewf.rs +++ b/crates/guara-core/tests/ewf.rs @@ -3,7 +3,9 @@ use guara_core::source::{open_path, read_vec, Source}; use std::path::PathBuf; fn fx(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ewf").join(name) + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/ewf") + .join(name) } fn raw() -> Vec { @@ -32,8 +34,14 @@ fn stored_hash_matches() { } let e = EwfSource::open(&fx("s_e6.E01")).unwrap(); assert_eq!(e.info.version, 1); - assert_eq!(e.info.fields.get("Examinador").map(String::as_str), Some("examiner")); - assert_eq!(e.info.fields.get("Número do caso").map(String::as_str), Some("case1")); + assert_eq!( + e.info.fields.get("Examinador").map(String::as_str), + Some("examiner") + ); + assert_eq!( + e.info.fields.get("Número do caso").map(String::as_str), + Some("case1") + ); assert_eq!(EwfSource::open(&fx("s_x7.Ex01")).unwrap().info.version, 2); } @@ -58,14 +66,20 @@ fn write_then_read_roundtrip_with_segments() { ..Default::default() }; let rep = write_e01(&src, &base, &opts, &|| false, &mut |_, _| {}).unwrap(); - assert!(dir.path().join("img.E02").exists(), "esperava mais de um segmento"); + assert!( + dir.path().join("img.E02").exists(), + "esperava mais de um segmento" + ); let r = open_path(&dir.path().join("img.E01")).unwrap(); assert_eq!(r.size(), data.len() as u64); assert!(read_vec(r.as_ref(), 0, data.len()).unwrap() == data); let e = EwfSource::open(&dir.path().join("img.E01")).unwrap(); assert_eq!(e.info.md5.as_deref(), Some(rep.md5.as_str())); assert_eq!(e.verify().unwrap(), Some(true)); - assert_eq!(e.info.fields.get("Examinador").map(String::as_str), Some("Lucas")); + assert_eq!( + e.info.fields.get("Examinador").map(String::as_str), + Some("Lucas") + ); } #[test] @@ -74,7 +88,17 @@ fn compressed_output_is_smaller_and_odd_sizes_work() { let src = guara_core::source::MemSource(data.clone()); let dir = tempfile::tempdir().unwrap(); let base = dir.path().join("z"); - write_e01(&src, &base, &E01Options { segment_size: 0, ..Default::default() }, &|| false, &mut |_, _| {}).unwrap(); + write_e01( + &src, + &base, + &E01Options { + segment_size: 0, + ..Default::default() + }, + &|| false, + &mut |_, _| {}, + ) + .unwrap(); assert!(std::fs::metadata(dir.path().join("z.E01")).unwrap().len() < 10_000); let r = open_path(&dir.path().join("z.E01")).unwrap(); assert_eq!(r.size(), 100_352); @@ -85,8 +109,22 @@ fn compressed_output_is_smaller_and_odd_sizes_work() { #[test] #[ignore] fn emit_for_external_verification() { - let Ok(base) = std::env::var("GUARA_E01_OUT") else { return }; + let Ok(base) = std::env::var("GUARA_E01_OUT") else { + return; + }; let src = guara_core::source::MemSource(raw()); - let opts = E01Options { segment_size: 100_000, case_number: "c1".into(), examiner: "ex".into(), ..Default::default() }; - write_e01(&src, std::path::Path::new(&base), &opts, &|| false, &mut |_, _| {}).unwrap(); + let opts = E01Options { + segment_size: 100_000, + case_number: "c1".into(), + examiner: "ex".into(), + ..Default::default() + }; + write_e01( + &src, + std::path::Path::new(&base), + &opts, + &|| false, + &mut |_, _| {}, + ) + .unwrap(); } diff --git a/crates/guara-core/tests/ext4.rs b/crates/guara-core/tests/ext4.rs index 360030b..f3ab418 100644 --- a/crates/guara-core/tests/ext4.rs +++ b/crates/guara-core/tests/ext4.rs @@ -17,7 +17,13 @@ fn have(tool: &str) -> bool { fn debugfs(img: &Path, cmds: &[&str]) -> String { let mut out = String::new(); for c in cmds { - let o = Command::new("debugfs").arg("-w").arg("-R").arg(c).arg(img).output().unwrap(); + let o = Command::new("debugfs") + .arg("-w") + .arg("-R") + .arg(c) + .arg(img) + .output() + .unwrap(); out.push_str(&String::from_utf8_lossy(&o.stdout)); out.push_str(&String::from_utf8_lossy(&o.stderr)); } @@ -26,7 +32,10 @@ fn debugfs(img: &Path, cmds: &[&str]) -> String { fn inode_of(img: &Path, path: &str) -> u64 { let o = debugfs(img, &[&format!("stat {path}")]); - let l = o.lines().find(|l| l.starts_with("Inode:")).unwrap_or_else(|| panic!("{o}")); + let l = o + .lines() + .find(|l| l.starts_with("Inode:")) + .unwrap_or_else(|| panic!("{o}")); l.split_whitespace().nth(1).unwrap().parse().unwrap() } @@ -76,13 +85,24 @@ fn build(opts: &[&str]) -> Option { .arg(&img) .output() .unwrap(); - assert!(st.status.success(), "{}", String::from_utf8_lossy(&st.stderr)); - Some(Img { _dir: dir, img, files }) + assert!( + st.status.success(), + "{}", + String::from_utf8_lossy(&st.stderr) + ); + Some(Img { + _dir: dir, + img, + files, + }) } fn deep(img: &Path) -> common::Collect { init(); - let opts = ScanOptions { mode: ScanMode::Deep, ..ScanOptions::default() }; + let opts = ScanOptions { + mode: ScanMode::Deep, + ..ScanOptions::default() + }; scan_path(img, &opts) } @@ -91,30 +111,60 @@ fn recovered(img: &Path, f: &Found) -> Vec { let rep = recover_all(img, std::slice::from_ref(f), d.path()); assert!(rep.failed.is_empty(), "{:?}", rep.failed); let p = &rep.recovered[0].path; - let p = if Path::new(p).is_absolute() { PathBuf::from(p) } else { d.path().join(p) }; + let p = if Path::new(p).is_absolute() { + PathBuf::from(p) + } else { + d.path().join(p) + }; std::fs::read(p).unwrap() } #[test] fn deleted_by_debugfs_keeps_extents_and_name() { - let Some(i) = build(&["-t", "ext4"]) else { return }; + let Some(i) = build(&["-t", "ext4"]) else { + return; + }; debugfs(&i.img, &["rm /docs/relatorio.dat", "rm /fotos/viagem.dat"]); let c = deep(&i.img); - for (p, data) in i.files.iter().filter(|f| f.0 == "docs/relatorio.dat" || f.0 == "fotos/viagem.dat") { + for (p, data) in i + .files + .iter() + .filter(|f| f.0 == "docs/relatorio.dat" || f.0 == "fotos/viagem.dat") + { let name = p.rsplit('/').next().unwrap(); - let f = c.found.iter().find(|f| f.name == name && f.origin == Origin::Ext).unwrap_or_else(|| panic!("{name} não encontrado: {:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::>())); + let f = c + .found + .iter() + .find(|f| f.name == name && f.origin == Origin::Ext) + .unwrap_or_else(|| { + panic!( + "{name} não encontrado: {:?}", + c.found + .iter() + .map(|f| (&f.name, f.origin)) + .collect::>() + ) + }); assert_eq!(f.size, data.len() as u64); - assert_eq!(f.dir.as_deref(), Some(&format!("/{}", p.rsplit_once('/').unwrap().0)[..])); + assert_eq!( + f.dir.as_deref(), + Some(&format!("/{}", p.rsplit_once('/').unwrap().0)[..]) + ); assert!(recovered(&i.img, f) == *data, "{name}"); } // O arquivo que não foi apagado não aparece. - assert!(!c.found.iter().any(|f| f.name == "fica.dat" && f.origin == Origin::Ext)); + assert!(!c + .found + .iter() + .any(|f| f.name == "fica.dat" && f.origin == Origin::Ext)); } #[test] fn truncated_inode_recovered_by_remnant_extents() { // Simula o que o kernel faz ao apagar: tamanho zerado e contagem de extents zerada. - let Some(i) = build(&["-t", "ext4"]) else { return }; + let Some(i) = build(&["-t", "ext4"]) else { + return; + }; let ino = inode_of(&i.img, "/docs/sub/contrato.dat"); debugfs( &i.img, @@ -125,7 +175,11 @@ fn truncated_inode_recovered_by_remnant_extents() { ], ); let c = deep(&i.img); - let f = c.found.iter().find(|f| f.name == "contrato.dat").expect("contrato.dat"); + let f = c + .found + .iter() + .find(|f| f.name == "contrato.dat") + .expect("contrato.dat"); assert_eq!(f.origin, Origin::Ext); // Tamanho exato é perdido: recupera todos os blocos (9000 bytes arredondados para 3 blocos de 4K). assert_eq!(f.size, 12288); @@ -138,7 +192,9 @@ fn journal_copy_restores_exact_size() { if !have("debugfs") { return; } - let Some(i) = build(&["-t", "ext4", "-O", "has_journal"]) else { return }; + let Some(i) = build(&["-t", "ext4", "-O", "has_journal"]) else { + return; + }; // Bloco da tabela de inodes que contém o inode do arquivo. let ino = inode_of(&i.img, "/fotos/viagem.dat"); let o = debugfs(&i.img, &["stats"]); @@ -157,9 +213,23 @@ fn journal_copy_restores_exact_size() { let bin = i.img.with_extension("blk"); std::fs::write(&bin, old).unwrap(); let script = i.img.with_extension("cmds"); - std::fs::write(&script, format!("jo\njw -b {blk} {}\njc\njc\n", bin.display())).unwrap(); - let o = Command::new("debugfs").arg("-w").arg("-f").arg(&script).arg(&i.img).output().unwrap(); - let j = format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)); + std::fs::write( + &script, + format!("jo\njw -b {blk} {}\njc\njc\n", bin.display()), + ) + .unwrap(); + let o = Command::new("debugfs") + .arg("-w") + .arg("-f") + .arg(&script) + .arg(&i.img) + .output() + .unwrap(); + let j = format!( + "{}{}", + String::from_utf8_lossy(&o.stdout), + String::from_utf8_lossy(&o.stderr) + ); if j.contains("Unknown request") || j.contains("not supported") { eprintln!("SKIP: debugfs sem suporte a escrita no diário: {j}"); return; @@ -174,7 +244,16 @@ fn journal_copy_restores_exact_size() { ], ); let c = deep(&i.img); - let f = c.found.iter().find(|f| f.name == "viagem.dat").unwrap_or_else(|| panic!("sem viagem.dat: {:?}", c.found.iter().map(|f| &f.name).collect::>())); + let f = c + .found + .iter() + .find(|f| f.name == "viagem.dat") + .unwrap_or_else(|| { + panic!( + "sem viagem.dat: {:?}", + c.found.iter().map(|f| &f.name).collect::>() + ) + }); assert_eq!(f.size, 700_000, "{:?}", f.note); assert!(recovered(&i.img, f) == i.files[2].1); } @@ -183,7 +262,9 @@ fn journal_copy_restores_exact_size() { fn ext2_blockmap_deleted_is_not_invented() { // ext2 zera os ponteiros ao apagar; sem diário não há o que recuperar por metadados, // mas a varredura não pode falhar nem listar o arquivo que continua existindo. - let Some(i) = build(&["-t", "ext2"]) else { return }; + let Some(i) = build(&["-t", "ext2"]) else { + return; + }; let c = deep(&i.img); assert!(!c.found.iter().any(|f| f.origin == Origin::Ext)); } diff --git a/crates/guara-core/tests/filevault.rs b/crates/guara-core/tests/filevault.rs index 7bb4d46..caf4dc2 100644 --- a/crates/guara-core/tests/filevault.rs +++ b/crates/guara-core/tests/filevault.rs @@ -7,7 +7,8 @@ use std::sync::Arc; #[test] fn opens_reference_image_and_matches_known_hash() { - let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/fvault2-images/small"); + let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../target/testdata/fvault2-images/small"); if !p.exists() { eprintln!("SKIP: execute scripts/fetch-testdata.sh"); return; @@ -31,5 +32,8 @@ fn opens_reference_image_and_matches_known_hash() { h.update(&buf[..n]); off += n as u64; } - assert_eq!(h.finalize().sha256, "2c662e36c0f7e2f5583e6a939bbcbdc660805692d0fccaa45ad4052beb3b8e18"); + assert_eq!( + h.finalize().sha256, + "2c662e36c0f7e2f5583e6a939bbcbdc660805692d0fccaa45ad4052beb3b8e18" + ); } diff --git a/crates/guara-core/tests/formats.rs b/crates/guara-core/tests/formats.rs index ec7783d..73a2878 100644 --- a/crates/guara-core/tests/formats.rs +++ b/crates/guara-core/tests/formats.rs @@ -16,31 +16,78 @@ fn check(path: &str, ext: &str, min_conf: Confidence) -> bool { // lixo depois do fim real (outro arquivo no disco) data.extend((0..5000u32).map(|i| (i.wrapping_mul(2654435761) >> 7) as u8 | 0x81)); let src = MemSource(data.clone()); - let h = detect(&src, 0, &data[..128], 1 << 30).unwrap_or_else(|| panic!("{path}: não reconhecido")); + let h = + detect(&src, 0, &data[..128], 1 << 30).unwrap_or_else(|| panic!("{path}: não reconhecido")); assert_eq!(h.len as usize, real, "{path}: tamanho"); assert_eq!(h.ext, ext, "{path}: extensão"); - assert!(h.confidence >= min_conf, "{path}: confiança {:?}", h.confidence); + assert!( + h.confidence >= min_conf, + "{path}: confiança {:?}", + h.confidence + ); true } fn first(dir: &str, ext: &str) -> Option { - let out = std::process::Command::new("find").args([dir, "-iname", &format!("*.{ext}"), "-type", "f", "-size", "+2k", "-size", "-20M"]).output().ok()?; - String::from_utf8_lossy(&out.stdout).lines().next().map(String::from) + let out = std::process::Command::new("find") + .args([ + dir, + "-iname", + &format!("*.{ext}"), + "-type", + "f", + "-size", + "+2k", + "-size", + "-20M", + ]) + .output() + .ok()?; + String::from_utf8_lossy(&out.stdout) + .lines() + .next() + .map(String::from) } #[test] fn real_files_have_exact_lengths() { let mut n = 0; let cases: Vec<(Option, &str, Confidence)> = vec![ - (first("/usr/share/cinnamon/sounds", "ogg"), "ogg", Confidence::High), + ( + first("/usr/share/cinnamon/sounds", "ogg"), + "ogg", + Confidence::High, + ), (first("/usr/share", "gz"), "gz", Confidence::High), (first("/usr/share/doc", "bz2"), "bz2", Confidence::High), (first("/usr/lib/firmware", "zst"), "zst", Confidence::Medium), - (first("/usr/share/texlive/texmf-dist/tex/latex/media9/players", "swf"), "swf", Confidence::Medium), - (first("/usr/share/code/resources/app", "rtf"), "rtf", Confidence::High), - (first("/usr/share/app-install/icons", "tiff"), "tif", Confidence::Medium), + ( + first( + "/usr/share/texlive/texmf-dist/tex/latex/media9/players", + "swf", + ), + "swf", + Confidence::Medium, + ), + ( + first("/usr/share/code/resources/app", "rtf"), + "rtf", + Confidence::High, + ), + ( + first("/usr/share/app-install/icons", "tiff"), + "tif", + Confidence::Medium, + ), (Some("/usr/bin/ls".to_string()), "elf", Confidence::High), - (first("/home/lucas/Documents/Desenvolvimento/pop-hub/public", "ico"), "ico", Confidence::High), + ( + first( + "/home/lucas/Documents/Desenvolvimento/pop-hub/public", + "ico", + ), + "ico", + Confidence::High, + ), ]; for (path, ext, conf) in cases { if let Some(p) = path { @@ -55,9 +102,24 @@ fn real_files_have_exact_lengths() { #[test] fn webm_rar_deb_with_unknown_locations() { for (dir, ext, out, conf) in [ - ("/media/lucas/d9d2ba04-f925-4b9a-97d7-92c2e3bfdb78/Nextcloud/apps/firstrunwizard/img", "webm", "webm", Confidence::High), - ("/home/lucas/Documents/Pessoal/dev/power_vision/.trabalho/libbsd", "deb", "deb", Confidence::Medium), - ("/media/lucas/d9d2ba04-f925-4b9a-97d7-92c2e3bfdb78/war_cloud/lucasrguerra/Backups", "rar", "rar", Confidence::High), + ( + "/media/lucas/d9d2ba04-f925-4b9a-97d7-92c2e3bfdb78/Nextcloud/apps/firstrunwizard/img", + "webm", + "webm", + Confidence::High, + ), + ( + "/home/lucas/Documents/Pessoal/dev/power_vision/.trabalho/libbsd", + "deb", + "deb", + Confidence::Medium, + ), + ( + "/media/lucas/d9d2ba04-f925-4b9a-97d7-92c2e3bfdb78/war_cloud/lucasrguerra/Backups", + "rar", + "rar", + Confidence::High, + ), ] { if let Some(p) = first(dir, ext) { check(&p, out, conf); diff --git a/crates/guara-core/tests/hfsplus.rs b/crates/guara-core/tests/hfsplus.rs index f48a4ec..414e0e3 100644 --- a/crates/guara-core/tests/hfsplus.rs +++ b/crates/guara-core/tests/hfsplus.rs @@ -5,7 +5,8 @@ use common::*; use guara_core::*; fn image() -> Option> { - let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/dfvfs/hfsplus.raw"); + let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../target/testdata/dfvfs/hfsplus.raw"); p.exists().then(|| std::fs::read(p).unwrap()) } @@ -14,7 +15,13 @@ fn scan_bytes(bytes: &[u8]) -> Collect { let dir = tempfile::tempdir().unwrap(); let p = dir.path().join("h.img"); std::fs::write(&p, bytes).unwrap(); - scan_path(&p, &ScanOptions { mode: ScanMode::Quick, ..Default::default() }) + scan_path( + &p, + &ScanOptions { + mode: ScanMode::Quick, + ..Default::default() + }, + ) } #[test] @@ -25,7 +32,11 @@ fn pristine_volume_has_no_deleted_files_and_is_recognized() { }; let c = scan_bytes(&img); assert_eq!(c.layout.as_ref().unwrap().partitions[0].fs, FsKind::HfsPlus); - assert!(c.found.is_empty(), "{:?}", c.found.iter().map(|f| &f.name).collect::>()); + assert!( + c.found.is_empty(), + "{:?}", + c.found.iter().map(|f| &f.name).collect::>() + ); } #[test] @@ -33,13 +44,17 @@ fn record_left_in_leaf_slack_is_recovered() { let Some(mut img) = image() else { return }; let bs = 4096usize; let node = 186 * bs + bs; // catálogo começa no bloco 186; nó 1 é a única folha - // Remove os 5 últimos registros como o driver faz ao apagar no fim do nó: só diminui a contagem. + // Remove os 5 últimos registros como o driver faz ao apagar no fim do nó: só diminui a contagem. img[node + 10..node + 12].copy_from_slice(&21u16.to_be_bytes()); // Libera o bloco de dados (bitmap de alocação no bloco 1). let blk = 278usize; img[bs + blk / 8] &= !(0x80 >> (blk % 8)); let c = scan_bytes(&img); - let f = c.found.iter().find(|f| f.name == "fseventsd-uuid").unwrap_or_else(|| panic!("{:?}", c.found.iter().map(|f| &f.name).collect::>())); + let f = c + .found + .iter() + .find(|f| f.name == "fseventsd-uuid") + .unwrap_or_else(|| panic!("{:?}", c.found.iter().map(|f| &f.name).collect::>())); assert_eq!(f.origin, Origin::Hfs); assert_eq!(f.size, 36); assert_eq!(f.dir.as_deref(), Some("/.fseventsd")); diff --git a/crates/guara-core/tests/jpegscan.rs b/crates/guara-core/tests/jpegscan.rs index a5fbc5a..ff9c580 100644 --- a/crates/guara-core/tests/jpegscan.rs +++ b/crates/guara-core/tests/jpegscan.rs @@ -11,14 +11,19 @@ fn photo(w: u16, h: u16) -> Vec { x ^= x << 17; let noise = ((x >> 40) & 31) as i32; let base = 90 + (xx * 90 / w as i32) + (yy * 60 / h as i32); - let blob = if ((xx - 200).pow(2) + (yy - 150).pow(2)) < 3600 { 70 } else { 0 }; + let blob = if ((xx - 200).pow(2) + (yy - 150).pow(2)) < 3600 { + 70 + } else { + 0 + }; let v = (base + blob + noise).clamp(0, 255) as u8; rgb.extend_from_slice(&[v, v.saturating_sub(20), (v as i32 / 2 + noise) as u8]); } } let mut out = vec![]; let enc = jpeg_encoder::Encoder::new(&mut out, 92); - enc.encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb).unwrap(); + enc.encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb) + .unwrap(); out } @@ -32,7 +37,10 @@ fn decodes_whole_image_and_ends_at_eoi() { assert_eq!(d.dc.len() as u32, h.total_mcus()); // O que sobra depois do último MCU é o EOI. let tail = &j[d.state.pos.min(j.len())..]; - assert!(tail.windows(2).any(|w| w == [0xFF, 0xD9]), "sem EOI depois do último MCU"); + assert!( + tail.windows(2).any(|w| w == [0xFF, 0xD9]), + "sem EOI depois do último MCU" + ); assert!(first_discontinuity(&h, &d.dc).is_none()); } @@ -59,7 +67,12 @@ fn detects_break_and_resumes_with_the_right_continuation() { // Decodificação segue o lixo por um tempo; mas a quebra aparece pelo erro ou pela descontinuidade. let mcu_break = first_discontinuity(&h, &d.dc); // MCUs bons: os decodificados ao fim do cluster `cut`. - let good = d.snaps.iter().filter(|s| s.pos <= cut).last().expect("snapshot antes da quebra").clone(); + let good = d + .snaps + .iter() + .rfind(|s| s.pos <= cut) + .expect("snapshot antes da quebra") + .clone(); // Retomada com a continuação verdadeira reproduz a decodificação inteira. let mut joined = j[..cut].to_vec(); joined.extend_from_slice(&j[cut..]); diff --git a/crates/guara-core/tests/luks.rs b/crates/guara-core/tests/luks.rs index 2da6db4..9bff054 100644 --- a/crates/guara-core/tests/luks.rs +++ b/crates/guara-core/tests/luks.rs @@ -25,8 +25,17 @@ fn py_ok() -> bool { fn run_stdin(cmd: &mut Command, input: &str) -> bool { use std::io::Write; - let mut ch = cmd.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn().unwrap(); - ch.stdin.take().unwrap().write_all(input.as_bytes()).unwrap(); + let mut ch = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + ch.stdin + .take() + .unwrap() + .write_all(input.as_bytes()) + .unwrap(); ch.wait().unwrap().success() } @@ -51,11 +60,26 @@ struct Case<'a> { } fn payload_offset(img: &Path, ver2: bool) -> u64 { - let o = Command::new("cryptsetup").arg("luksDump").arg(img).output().unwrap(); + let o = Command::new("cryptsetup") + .arg("luksDump") + .arg(img) + .output() + .unwrap(); let t = String::from_utf8_lossy(&o.stdout).to_string(); if ver2 { - let l = t.lines().skip_while(|l| !l.starts_with("Data segments")).find(|l| l.contains("offset:")).unwrap(); - l.split(':').nth(1).unwrap().split_whitespace().next().unwrap().parse().unwrap() + let l = t + .lines() + .skip_while(|l| !l.starts_with("Data segments")) + .find(|l| l.contains("offset:")) + .unwrap(); + l.split(':') + .nth(1) + .unwrap() + .split_whitespace() + .next() + .unwrap() + .parse() + .unwrap() } else { let l = t.lines().find(|l| l.starts_with("Payload offset")).unwrap(); l.split(':').nth(1).unwrap().trim().parse::().unwrap() * 512 @@ -72,7 +96,10 @@ fn run_case(c: &Case) { let plain = dir.path().join("plain.bin"); let data = plain_data(3 * 1024 * 1024 + 4096); std::fs::write(&plain, &data).unwrap(); - std::fs::File::create(&img).unwrap().set_len(24 << 20).unwrap(); + std::fs::File::create(&img) + .unwrap() + .set_len(24 << 20) + .unwrap(); let pw = "senha com acentuação ç"; let mut fmt = Command::new("cryptsetup"); fmt.args(["luksFormat", "--batch-mode", "--key-file", "-"]); @@ -81,9 +108,20 @@ fn run_case(c: &Case) { assert!(run_stdin(&mut fmt, pw), "luksFormat falhou ({})", c.name); let vkf = dir.path().join("vk.bin"); let mut dump = Command::new("cryptsetup"); - dump.args(["luksDump", "--dump-volume-key", "--batch-mode", "--key-file", "-", "--volume-key-file"]); + dump.args([ + "luksDump", + "--dump-volume-key", + "--batch-mode", + "--key-file", + "-", + "--volume-key-file", + ]); dump.arg(&vkf).arg(&img); - assert!(run_stdin(&mut dump, pw), "dump da chave falhou ({})", c.name); + assert!( + run_stdin(&mut dump, pw), + "dump da chave falhou ({})", + c.name + ); let off = payload_offset(&img, c.ver2); let st = Command::new("python3") .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/support/luks_encrypt.py")) @@ -104,21 +142,36 @@ fn run_case(c: &Case) { Err(e) => panic!("{}: erro inesperado {e}", c.name), Ok(_) => panic!("{}: abriu com senha errada", c.name), } - let u = luks::unlock(src.clone(), &[Secret::Password("x".into()), Secret::Password(pw.into())]) - .unwrap_or_else(|e| panic!("{}: {e}", c.name)); + let u = luks::unlock( + src.clone(), + &[Secret::Password("x".into()), Secret::Password(pw.into())], + ) + .unwrap_or_else(|e| panic!("{}: {e}", c.name)); let got = read_vec(u.source.as_ref(), 0, data.len()).unwrap(); assert!(got == data, "{}: conteúdo difere", c.name); // Também abre com a chave do volume. let vk = std::fs::read(&vkf).unwrap(); let u2 = luks::unlock(src, &[Secret::VolumeKey(vk)]).unwrap(); - assert_eq!(read_vec(u2.source.as_ref(), 1000, 5000).unwrap(), &data[1000..6000]); + assert_eq!( + read_vec(u2.source.as_ref(), 1000, 5000).unwrap(), + &data[1000..6000] + ); } #[test] fn luks1_xts() { run_case(&Case { name: "luks1-xts", - format_args: vec!["--type", "luks1", "--pbkdf-force-iterations", "1000", "-c", "aes-xts-plain64", "-s", "512"], + format_args: vec![ + "--type", + "luks1", + "--pbkdf-force-iterations", + "1000", + "-c", + "aes-xts-plain64", + "-s", + "512", + ], cipher: "aes-xts-plain64", sector: 512, ver2: false, @@ -129,7 +182,18 @@ fn luks1_xts() { fn luks1_cbc_essiv_sha1() { run_case(&Case { name: "luks1-cbc-essiv", - format_args: vec!["--type", "luks1", "--pbkdf-force-iterations", "1000", "-c", "aes-cbc-essiv:sha256", "-s", "256", "-h", "sha1"], + format_args: vec![ + "--type", + "luks1", + "--pbkdf-force-iterations", + "1000", + "-c", + "aes-cbc-essiv:sha256", + "-s", + "256", + "-h", + "sha1", + ], cipher: "aes-cbc-essiv:sha256", sector: 512, ver2: false, @@ -140,7 +204,20 @@ fn luks1_cbc_essiv_sha1() { fn luks2_argon2id_xts() { run_case(&Case { name: "luks2-argon2id", - format_args: vec!["--type", "luks2", "--pbkdf", "argon2id", "--pbkdf-force-iterations", "4", "--pbkdf-memory", "64", "-c", "aes-xts-plain64", "-s", "512"], + format_args: vec![ + "--type", + "luks2", + "--pbkdf", + "argon2id", + "--pbkdf-force-iterations", + "4", + "--pbkdf-memory", + "64", + "-c", + "aes-xts-plain64", + "-s", + "512", + ], cipher: "aes-xts-plain64", sector: 4096, ver2: true, @@ -151,7 +228,20 @@ fn luks2_argon2id_xts() { fn luks2_pbkdf2_sector512_aes256() { run_case(&Case { name: "luks2-pbkdf2", - format_args: vec!["--type", "luks2", "--pbkdf", "pbkdf2", "--pbkdf-force-iterations", "1000", "-c", "aes-xts-plain64", "-s", "256", "--sector-size", "512"], + format_args: vec![ + "--type", + "luks2", + "--pbkdf", + "pbkdf2", + "--pbkdf-force-iterations", + "1000", + "-c", + "aes-xts-plain64", + "-s", + "256", + "--sector-size", + "512", + ], cipher: "aes-xts-plain64", sector: 512, ver2: true, diff --git a/crates/guara-core/tests/lvm.rs b/crates/guara-core/tests/lvm.rs index 4997c6e..0650e87 100644 --- a/crates/guara-core/tests/lvm.rs +++ b/crates/guara-core/tests/lvm.rs @@ -7,7 +7,8 @@ use std::sync::Arc; #[test] fn assembles_real_lvm_image() { - let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/dfvfs/lvm.raw"); + let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../target/testdata/dfvfs/lvm.raw"); if !p.exists() { eprintln!("SKIP: execute scripts/fetch-testdata.sh"); return; diff --git a/crates/guara-core/tests/mp4rebuild.rs b/crates/guara-core/tests/mp4rebuild.rs index 762bb40..8727975 100644 --- a/crates/guara-core/tests/mp4rebuild.rs +++ b/crates/guara-core/tests/mp4rebuild.rs @@ -39,11 +39,20 @@ const SPS: &str = "67f4000d90d9b28283f602d41818190000030001000003003c8f142996"; const PPS: &str = "68ebec448440"; fn hex(s: &str) -> Vec { - (0..s.len() / 2).map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap()).collect() + (0..s.len() / 2) + .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap()) + .collect() } /// Cabeçalho de fatia H.264 conforme o SPS acima (log2_max_frame_num e log2_max_poc_lsb lidos do SPS). -fn slice(idr: bool, frame_num: u32, poc_lsb: u32, payload: usize, seed: u8, sps: &mguara::Sps) -> Vec { +fn slice( + idr: bool, + frame_num: u32, + poc_lsb: u32, + payload: usize, + seed: u8, + sps: &mguara::Sps, +) -> Vec { let mut b = BitW::new(); b.ue(0); // first_mb_in_slice b.ue(if idr { 7 } else { 5 }); // slice_type @@ -95,7 +104,14 @@ fn synthetic_opt(gops: usize, inband: bool) -> (Vec, Vec, Vec) if i == 0 { frame_num = 0; } - let s = slice(i == 0, frame_num % 16, p % 64, 600 + i * 90 + g * 7, (g * 7 + i) as u8, &sps); + let s = slice( + i == 0, + frame_num % 16, + p % 64, + 600 + i * 90 + g * 7, + (g * 7 + i) as u8, + &sps, + ); nal(&mut mdat, &s); if p % 4 == 0 || i == 1 || i == 4 { frame_num += 1; @@ -105,7 +121,10 @@ fn synthetic_opt(gops: usize, inband: bool) -> (Vec, Vec, Vec) } } let mut f = vec![]; - f.extend_from_slice(&[0, 0, 0, 24, b'f', b't', b'y', b'p', b'i', b's', b'o', b'm', 0, 0, 2, 0, b'i', b's', b'o', b'm', b'a', b'v', b'c', b'1']); + f.extend_from_slice(&[ + 0, 0, 0, 24, b'f', b't', b'y', b'p', b'i', b's', b'o', b'm', 0, 0, 2, 0, b'i', b's', b'o', + b'm', b'a', b'v', b'c', b'1', + ]); f.extend_from_slice(&[0, 0, 0, 8, b'f', b'r', b'e', b'e']); f.extend_from_slice(&((8 + mdat.len()) as u32).to_be_bytes()); f.extend_from_slice(b"mdat"); @@ -133,10 +152,14 @@ fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() { let r = mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).expect("reconstrução"); assert_eq!(r.frames, sizes.len()); assert_eq!((r.width, r.height), (320, 240)); - assert_eq!(r.payload.1 as usize, real_len, "o fim do vídeo foi detectado"); + assert_eq!( + r.payload.1 as usize, real_len, + "o fim do vídeo foi detectado" + ); let out = assemble(&file, &r); let size = out.len() as u64; - let mut mp4 = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), size).expect("moov válido"); + let mut mp4 = + mp4::Mp4Reader::read_header(std::io::Cursor::new(out), size).expect("moov válido"); let track_id = *mp4.tracks().keys().next().unwrap(); let t = &mp4.tracks()[&track_id]; assert_eq!(t.sample_count() as usize, sizes.len()); @@ -164,7 +187,9 @@ fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() { #[test] fn rebuilds_real_h264_file_like_the_original_index() { let Ok(path) = std::env::var("GUARA_MP4_SAMPLE") else { - eprintln!("SKIP: defina GUARA_MP4_SAMPLE com um MP4 H.264 para comparar com o índice original"); + eprintln!( + "SKIP: defina GUARA_MP4_SAMPLE com um MP4 H.264 para comparar com o índice original" + ); return; }; let orig = std::fs::read(&path).unwrap(); @@ -189,7 +214,10 @@ fn rebuilds_real_h264_file_like_the_original_index() { let mut b = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), rsize).unwrap(); let ta = *a.tracks().keys().next().unwrap(); let tb = *b.tracks().keys().next().unwrap(); - assert_eq!(a.tracks()[&ta].sample_count(), b.tracks()[&tb].sample_count()); + assert_eq!( + a.tracks()[&ta].sample_count(), + b.tracks()[&tb].sample_count() + ); for i in 1..=a.tracks()[&ta].sample_count() { let x = a.read_sample(ta, i).unwrap().unwrap(); let y = b.read_sample(tb, i).unwrap().unwrap(); @@ -243,8 +271,12 @@ fn rebuilds_real_video_with_interleaved_audio() { // O tamanho do mdat declarado vai muito além dos 40 MB que temos (arquivo cortado). let src = MemSource(file.clone()); // Sem SPS/PPS no mdat, é preciso um vídeo de referência da mesma câmera (aqui, o próprio original). - let cfg = mp4rebuild::config_from_mp4(&MemSource(head.clone()), 0, head.len() as u64).expect("avcC do original"); - assert!(mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).is_none(), "sem referência não há como montar o avcC"); + let cfg = mp4rebuild::config_from_mp4(&MemSource(head.clone()), 0, head.len() as u64) + .expect("avcC do original"); + assert!( + mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).is_none(), + "sem referência não há como montar o avcC" + ); let r = mp4rebuild::rebuild(&src, 0, file.len() as u64, &[cfg]).expect("reconstrução"); let out = assemble(&file, &r); let osize = out.len() as u64; @@ -253,10 +285,26 @@ fn rebuilds_real_video_with_interleaved_audio() { // Original (índice completo) para comparar as primeiras amostras. let f = std::fs::File::open(&path).unwrap(); let mut a = mp4::Mp4Reader::read_header(std::io::BufReader::new(f), total).unwrap(); - let vid = a.tracks().values().find(|t| t.track_type().map(|x| x == mp4::TrackType::Video).unwrap_or(false)).unwrap().track_id(); - eprintln!("reconstruído: {} quadros, {} bytes pulados (áudio), {}x{}", r.frames, r.skipped_bytes, r.width, r.height); - let sizes_a: Vec = (1..=60).map(|i| a.read_sample(vid, i).unwrap().unwrap().bytes.len()).collect(); - let sizes_b: Vec = (1..=60).map(|i| b.read_sample(tb, i).unwrap().unwrap().bytes.len()).collect(); + let vid = a + .tracks() + .values() + .find(|t| { + t.track_type() + .map(|x| x == mp4::TrackType::Video) + .unwrap_or(false) + }) + .unwrap() + .track_id(); + eprintln!( + "reconstruído: {} quadros, {} bytes pulados (áudio), {}x{}", + r.frames, r.skipped_bytes, r.width, r.height + ); + let sizes_a: Vec = (1..=60) + .map(|i| a.read_sample(vid, i).unwrap().unwrap().bytes.len()) + .collect(); + let sizes_b: Vec = (1..=60) + .map(|i| b.read_sample(tb, i).unwrap().unwrap().bytes.len()) + .collect(); eprintln!("ORIG {:?}\nMINE {:?}", &sizes_a[40..60], &sizes_b[40..60]); let check = (r.frames as u32 - 1).min(400); for i in 1..=check { @@ -275,7 +323,8 @@ fn deep_scan_rebuilds_a_video_without_moov_using_a_neighbour_as_reference() { common::init(); // Arquivo íntegro (com moov) da "mesma câmera" e um segundo que perdeu o moov e não traz SPS/PPS. let (good_raw, _, _) = synthetic_opt(8, true); - let r = mp4rebuild::rebuild(&MemSource(good_raw.clone()), 0, good_raw.len() as u64, &[]).unwrap(); + let r = + mp4rebuild::rebuild(&MemSource(good_raw.clone()), 0, good_raw.len() as u64, &[]).unwrap(); let good = assemble(&good_raw, &r); let (broken, sizes, _) = synthetic_opt(10, false); let mut disk = vec![0u8; 8192]; @@ -288,12 +337,32 @@ fn deep_scan_rebuilds_a_video_without_moov_using_a_neighbour_as_reference() { let dir = tempfile::tempdir().unwrap(); let img = dir.path().join("v.img"); std::fs::write(&img, &disk).unwrap(); - let c = common::scan_path(&img, &ScanOptions { mode: ScanMode::Deep, ..Default::default() }); + let c = common::scan_path( + &img, + &ScanOptions { + mode: ScanMode::Deep, + ..Default::default() + }, + ); let vids: Vec<_> = c.found.iter().filter(|f| f.ext == "mp4").collect(); - assert_eq!(vids.len(), 2, "{:?}", vids.iter().map(|f| (f.first_offset(), f.size, f.confidence, &f.note)).collect::>()); - let fb = vids.iter().find(|f| matches!(f.data, guara_core::DataLoc::Parts { .. })).expect("vídeo quebrado"); + assert_eq!( + vids.len(), + 2, + "{:?}", + vids.iter() + .map(|f| (f.first_offset(), f.size, f.confidence, &f.note)) + .collect::>() + ); + let fb = vids + .iter() + .find(|f| matches!(f.data, guara_core::DataLoc::Parts { .. })) + .expect("vídeo quebrado"); let _ = broken_off; - assert!(fb.note.as_deref().unwrap_or("").contains("reconstruído"), "{:?}", fb.note); + assert!( + fb.note.as_deref().unwrap_or("").contains("reconstruído"), + "{:?}", + fb.note + ); let _ = good_off; let out = dir.path().join("out"); let rep = common::recover_all(&img, std::slice::from_ref(*fb), &out); diff --git a/crates/guara-core/tests/raid.rs b/crates/guara-core/tests/raid.rs index 7eaff67..7c84375 100644 --- a/crates/guara-core/tests/raid.rs +++ b/crates/guara-core/tests/raid.rs @@ -59,7 +59,9 @@ fn encode_parity(level: Level, layout: Layout, n: usize, logical: &[u8]) -> Vec< let start = if level == Level::R6 { qd + 1 } else { pd + 1 }; (0..dd).map(|d| (start + d) % n).collect() } else { - (0..n).filter(|&i| i != pd && !(level == Level::R6 && i == qd)).collect() + (0..n) + .filter(|&i| i != pd && !(level == Level::R6 && i == qd)) + .collect() }; let mut p = vec![0u8; C]; let mut q = vec![0u8; C]; @@ -83,7 +85,10 @@ fn members(imgs: &[Vec], missing: &[usize]) -> Vec> { imgs.iter() .enumerate() .map(|(i, v)| { - (!missing.contains(&i)).then(|| Member { src: Arc::new(MemSource(v.clone())), data_offset: 0 }) + (!missing.contains(&i)).then(|| Member { + src: Arc::new(MemSource(v.clone())), + data_offset: 0, + }) }) .collect() } @@ -92,10 +97,19 @@ fn check(level: Level, layout: Layout, n: usize, missing: &[usize]) { let dd = if level == Level::R6 { n - 2 } else { n - 1 }; let logical = data(C * dd * 23); let imgs = encode_parity(level, layout, n, &logical); - let r = RaidSource::new(RaidParams::simple(level, C as u64, layout, members(&imgs, missing))).unwrap(); + let r = RaidSource::new(RaidParams::simple( + level, + C as u64, + layout, + members(&imgs, missing), + )) + .unwrap(); assert_eq!(r.size(), logical.len() as u64); let got = read_vec(&r, 0, logical.len()).unwrap(); - assert!(got == logical, "{level:?} {layout:?} n={n} faltando {missing:?}"); + assert!( + got == logical, + "{level:?} {layout:?} n={n} faltando {missing:?}" + ); // leitura desalinhada let part = read_vec(&r, 5000, 30000).unwrap(); assert_eq!(part, &logical[5000..35000]); @@ -136,15 +150,33 @@ fn raid0_raid1_linear_and_raid10_near() { for (c, ch) in logical.chunks(C).enumerate() { imgs[c % 3].extend_from_slice(ch); } - let r = RaidSource::new(RaidParams::simple(Level::R0, C as u64, Layout::LeftSymmetric, members(&imgs, &[]))).unwrap(); + let r = RaidSource::new(RaidParams::simple( + Level::R0, + C as u64, + Layout::LeftSymmetric, + members(&imgs, &[]), + )) + .unwrap(); assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical); // RAID 1, um membro caído let m = vec![logical.clone(), logical.clone()]; - let r = RaidSource::new(RaidParams::simple(Level::R1, C as u64, Layout::LeftSymmetric, members(&m, &[0]))).unwrap(); + let r = RaidSource::new(RaidParams::simple( + Level::R1, + C as u64, + Layout::LeftSymmetric, + members(&m, &[0]), + )) + .unwrap(); assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical); // Linear let (a, b) = logical.split_at(C * 10); - let r = RaidSource::new(RaidParams::simple(Level::Linear, C as u64, Layout::LeftSymmetric, members(&[a.to_vec(), b.to_vec()], &[]))).unwrap(); + let r = RaidSource::new(RaidParams::simple( + Level::Linear, + C as u64, + Layout::LeftSymmetric, + members(&[a.to_vec(), b.to_vec()], &[]), + )) + .unwrap(); assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical); // RAID 10 near=2 com 4 discos: slots em sequência. let n = 4; @@ -156,7 +188,12 @@ fn raid0_raid1_linear_and_raid10_near() { imgs[disk][row * C..(row + 1) * C].copy_from_slice(ch); } } - let mut p = RaidParams::simple(Level::R10, C as u64, Layout::LeftSymmetric, members(&imgs, &[1])); + let mut p = RaidParams::simple( + Level::R10, + C as u64, + Layout::LeftSymmetric, + members(&imgs, &[1]), + ); p.near = 2; p.far = 1; let r = RaidSource::new(p).unwrap(); @@ -164,7 +201,18 @@ fn raid0_raid1_linear_and_raid10_near() { assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical); } -fn md_v1_super(uuid: u8, level: u32, layout: u32, raid_disks: u32, dev_number: u32, role: u16, data_off_sectors: u64, data_sectors: u64, events: u64) -> Vec { +#[allow(clippy::too_many_arguments)] +fn md_v1_super( + uuid: u8, + level: u32, + layout: u32, + raid_disks: u32, + dev_number: u32, + role: u16, + data_off_sectors: u64, + data_sectors: u64, + events: u64, +) -> Vec { let mut b = vec![0u8; 4096]; b[0..4].copy_from_slice(&0xA92B_4EFCu32.to_le_bytes()); b[4..8].copy_from_slice(&1u32.to_le_bytes()); @@ -199,7 +247,17 @@ fn assembles_from_md_v12_superblocks_with_a_missing_disk() { continue; // disco perdido } let mut full = vec![0u8; 4096 + (data_off as usize) * 512 + im.len()]; - let sb = md_v1_super(7, 5, 2, n as u32, i as u32, i as u16, data_off, (im.len() / 512) as u64, 100); + let sb = md_v1_super( + 7, + 5, + 2, + n as u32, + i as u32, + i as u16, + data_off, + (im.len() / 512) as u64, + 100, + ); full[4096..8192].copy_from_slice(&sb); full[4096 + data_off as usize * 512..][..im.len()].copy_from_slice(im); // o superbloco 1.2 fica a 4 KiB; os dados começam em data_offset a partir do início diff --git a/crates/guara-core/tests/raid_detect.rs b/crates/guara-core/tests/raid_detect.rs index 39f0e3e..34b001b 100644 --- a/crates/guara-core/tests/raid_detect.rs +++ b/crates/guara-core/tests/raid_detect.rs @@ -7,7 +7,11 @@ use std::process::Command; use std::sync::Arc; fn have(t: &str) -> bool { - Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false) + Command::new("sh") + .args(["-c", &format!("command -v {t}")]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) } fn ext4_image(mib: u64) -> Option> { @@ -19,18 +23,33 @@ fn ext4_image(mib: u64) -> Option> { for d in 0..12 { std::fs::create_dir_all(root.join(format!("pasta{d}/sub"))).unwrap(); for f in 0..40 { - std::fs::write(root.join(format!("pasta{d}/arq{f}.txt")), vec![b'a' + (f % 26) as u8; 300 + f * 37]).unwrap(); + std::fs::write( + root.join(format!("pasta{d}/arq{f}.txt")), + vec![b'a' + (f % 26) as u8; 300 + f * 37], + ) + .unwrap(); } } let img = dir.path().join("e.img"); - std::fs::File::create(&img).unwrap().set_len(mib << 20).unwrap(); - assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-b", "1024", "-d"]).arg(&root).arg(&img).status().unwrap().success()); + std::fs::File::create(&img) + .unwrap() + .set_len(mib << 20) + .unwrap(); + assert!(Command::new("mke2fs") + .args(["-q", "-F", "-t", "ext4", "-b", "1024", "-d"]) + .arg(&root) + .arg(&img) + .status() + .unwrap() + .success()); Some(std::fs::read(img).unwrap()) } #[test] fn finds_raid5_chunk_layout_and_shuffled_order() { - let Some(logical) = ext4_image(24) else { return }; + let Some(logical) = ext4_image(24) else { + return; + }; let (n, chunk) = (3usize, 32768usize); let dd = n - 1; let rows = logical.len() / (chunk * dd); @@ -51,11 +70,17 @@ fn finds_raid5_chunk_layout_and_shuffled_order() { disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p); } // Entrega os discos embaralhados: fornecidos = [2, 0, 1] do array real. - let given: Vec> = [2usize, 0, 1].iter().map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc).collect(); + let given: Vec> = [2usize, 0, 1] + .iter() + .map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc) + .collect(); let found = raid_detect::detect(&given, &|| false); assert!(!found.is_empty()); for c in found.iter().take(6) { - eprintln!("CAND {:?} {} {:?} {:?} {:.2}", c.level, c.chunk, c.layout, c.order, c.score); + eprintln!( + "CAND {:?} {} {:?} {:?} {:.2}", + c.level, c.chunk, c.layout, c.order, c.score + ); } let best = &found[0]; assert_eq!(best.level, Level::R5); @@ -63,19 +88,30 @@ fn finds_raid5_chunk_layout_and_shuffled_order() { // Layouts e ordens diferentes podem ser equivalentes; o que importa é o conteúdo montado. let r = guara_core::raid::RaidSource::new(best.params(&given)).unwrap(); let got = guara_core::source::read_vec(&r, 0, logical.len()).unwrap(); - assert!(got == logical, "montagem escolhida ({:?}, {:?}, {:?}) não reproduz o volume", best.layout, best.order, best.level); + assert!( + got == logical, + "montagem escolhida ({:?}, {:?}, {:?}) não reproduz o volume", + best.layout, + best.order, + best.level + ); } #[test] fn finds_raid0() { - let Some(logical) = ext4_image(24) else { return }; + let Some(logical) = ext4_image(24) else { + return; + }; let (n, chunk) = (2usize, 65536usize); let rows = logical.len() / (chunk * n); let mut disks = vec![vec![]; n]; for c in 0..rows * n { disks[c % n].extend_from_slice(&logical[c * chunk..(c + 1) * chunk]); } - let given: Vec> = [1usize, 0].iter().map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc).collect(); + let given: Vec> = [1usize, 0] + .iter() + .map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc) + .collect(); let found = raid_detect::detect(&given, &|| false); let best = &found[0]; assert_eq!(best.level, Level::R0); diff --git a/crates/guara-core/tests/xfs.rs b/crates/guara-core/tests/xfs.rs index 2efe07d..9062597 100644 --- a/crates/guara-core/tests/xfs.rs +++ b/crates/guara-core/tests/xfs.rs @@ -8,7 +8,11 @@ use std::path::PathBuf; use std::process::Command; fn have(t: &str) -> bool { - Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false) + Command::new("sh") + .args(["-c", &format!("command -v {t}")]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) } fn data(seed: u64, n: usize) -> Vec { @@ -36,7 +40,11 @@ fn be64(b: &[u8], o: usize) -> u64 { fn unpack(r: &[u8]) -> (u64, u64, u64) { let hi = be64(r, 0); let lo = be64(r, 8); - ((hi >> 9) & ((1 << 54) - 1), ((hi & 0x1FF) << 43) | (lo >> 21), lo & 0x1F_FFFF) + ( + (hi >> 9) & ((1 << 54) - 1), + ((hi & 0x1FF) << 43) | (lo >> 21), + lo & 0x1F_FFFF, + ) } #[test] @@ -55,7 +63,11 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() { std::fs::write(src.join(format!("a_longa_{i}.txt")), format!("arquivo {i}")).unwrap(); } let mut proto = String::from("/dev/null\n0 0\nd--755 0 0\ndocs d--755 0 0\n"); - let mut names: Vec<_> = std::fs::read_dir(&src).unwrap().flatten().map(|e| e.file_name().to_string_lossy().to_string()).collect(); + let mut names: Vec<_> = std::fs::read_dir(&src) + .unwrap() + .flatten() + .map(|e| e.file_name().to_string_lossy().to_string()) + .collect(); names.sort(); for n in &names { proto.push_str(&format!(" {n} ---644 0 0 {}\n", src.join(n).display())); @@ -64,8 +76,16 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() { let pf = dir.path().join("proto"); std::fs::write(&pf, proto).unwrap(); let img = dir.path().join("x.img"); - std::fs::File::create(&img).unwrap().set_len(320 << 20).unwrap(); - let o = Command::new("mkfs.xfs").args(["-q", "-f", "-p"]).arg(&pf).arg(&img).output().unwrap(); + std::fs::File::create(&img) + .unwrap() + .set_len(320 << 20) + .unwrap(); + let o = Command::new("mkfs.xfs") + .args(["-q", "-f", "-p"]) + .arg(&pf) + .arg(&img) + .output() + .unwrap(); assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr)); let db = |cmds: &[&str]| -> String { @@ -78,11 +98,39 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() { String::from_utf8_lossy(&c.output().unwrap().stdout).to_string() }; let nc = db(&["blockget -n", "ncheck"]); - let ino: u64 = nc.lines().find(|l| l.ends_with("docs/relatorio.dat")).unwrap().split_whitespace().next().unwrap().parse().unwrap(); + let ino: u64 = nc + .lines() + .find(|l| l.ends_with("docs/relatorio.dat")) + .unwrap() + .split_whitespace() + .next() + .unwrap() + .parse() + .unwrap(); let conv = db(&[&format!("convert ino {ino} byte")]); - let ino_off = usize::from_str_radix(conv.split("0x").nth(1).unwrap().split_whitespace().next().unwrap().trim_end_matches(')'), 16).unwrap(); + let ino_off = usize::from_str_radix( + conv.split("0x") + .nth(1) + .unwrap() + .split_whitespace() + .next() + .unwrap() + .trim_end_matches(')'), + 16, + ) + .unwrap(); let sbv = db(&["sb 0", "p agblocks agblklog inopblog"]); - let num = |k: &str| -> u64 { sbv.lines().find(|l| l.starts_with(k)).unwrap().split('=').nth(1).unwrap().trim().parse().unwrap() }; + let num = |k: &str| -> u64 { + sbv.lines() + .find(|l| l.starts_with(k)) + .unwrap() + .split('=') + .nth(1) + .unwrap() + .trim() + .parse() + .unwrap() + }; let (agblocks, agblklog, inopblog) = (num("agblocks"), num("agblklog"), num("inopblog")); let mut b = std::fs::read(&img).unwrap(); @@ -90,8 +138,10 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() { // extents do arquivo (no inode, formato 3) assert_eq!(b[ino_off + 5], 2); let nex = be32(&b[ino_off..], 76) as usize; - let exts: Vec<_> = (0..nex).map(|i| unpack(&b[ino_off + 176 + i * 16..])).collect(); - assert_eq!(exts.iter().map(|e| e.2).sum::() * 4096 >= 150_000, true); + let exts: Vec<_> = (0..nex) + .map(|i| unpack(&b[ino_off + 176 + i * 16..])) + .collect(); + assert!(exts.iter().map(|e| e.2).sum::() * 4096 >= 150_000); // 1) libera o inode como o kernel: modo 0, links 0, tamanho 0, 0 extents b[ino_off + 2..ino_off + 4].fill(0); b[ino_off + 16..ino_off + 20].fill(0); @@ -125,7 +175,14 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() { assert_eq!(blevel, 1); let bb = ag_off + broot * bs; let nrec = be16(&b[bb..], 6) as usize; - let mut recs: Vec<(u32, u32)> = (0..nrec).map(|i| (be32(&b[bb + 56 + i * 8..], 0), be32(&b[bb + 56 + i * 8..], 4))).collect(); + let mut recs: Vec<(u32, u32)> = (0..nrec) + .map(|i| { + ( + be32(&b[bb + 56 + i * 8..], 0), + be32(&b[bb + 56 + i * 8..], 4), + ) + }) + .collect(); for e in &exts { recs.push(((e.1 & ((1 << agblklog) - 1)) as u32, e.2 as u32)); } @@ -144,18 +201,38 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() { b[bb + 6..bb + 8].copy_from_slice(&(merged.len() as u16).to_be_bytes()); // 4) entrada de diretório liberada: perde os 4 primeiros bytes do inode (viram marca + tamanho) let name = b"relatorio.dat"; - let pos = (0..b.len() - 20).find(|&p| &b[p + 9..p + 9 + name.len()] == name && b[p + 8] as usize == name.len() && be64(&b[p..], 0) == ino).expect("entrada do diretório"); + let pos = (0..b.len() - 20) + .find(|&p| { + &b[p + 9..p + 9 + name.len()] == name + && b[p + 8] as usize == name.len() + && be64(&b[p..], 0) == ino + }) + .expect("entrada do diretório"); let size = (8 + 1 + name.len() + 1 + 2usize).div_ceil(8) * 8; b[pos..pos + 2].copy_from_slice(&[0xFF, 0xFF]); b[pos + 2..pos + 4].copy_from_slice(&(size as u16).to_be_bytes()); std::fs::write(&img, &b).unwrap(); - let c = scan_path(&img, &ScanOptions { mode: ScanMode::Deep, ..Default::default() }); + let c = scan_path( + &img, + &ScanOptions { + mode: ScanMode::Deep, + ..Default::default() + }, + ); let f = c .found .iter() .find(|f| f.origin == Origin::Xfs) - .unwrap_or_else(|| panic!("nada do XFS: {:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::>())); + .unwrap_or_else(|| { + panic!( + "nada do XFS: {:?}", + c.found + .iter() + .map(|f| (&f.name, f.origin)) + .collect::>() + ) + }); assert_eq!(f.name, "relatorio.dat"); assert_eq!(f.dir.as_deref(), Some("/docs")); assert!(f.confidence >= Confidence::Medium, "{:?}", f.note); @@ -168,7 +245,10 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() { assert!(got.starts_with(&doc), "conteúdo inicial difere"); assert_eq!(got.len() % 4096, 0); // Só um arquivo apagado. - assert_eq!(c.found.iter().filter(|f| f.origin == Origin::Xfs).count(), 1); + assert_eq!( + c.found.iter().filter(|f| f.origin == Origin::Xfs).count(), + 1 + ); } #[test] @@ -178,7 +258,13 @@ fn reference_image_is_recognized() { return; } init(); - let c = scan_path(&p, &ScanOptions { mode: ScanMode::Quick, ..Default::default() }); + let c = scan_path( + &p, + &ScanOptions { + mode: ScanMode::Quick, + ..Default::default() + }, + ); let l = c.layout.unwrap(); assert_eq!(l.partitions[0].fs, FsKind::Xfs); }