From 5d45273e0dc39e111f7dfaa8df38dbd1dd66f492 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Sun, 18 Oct 2026 23:24:41 -0300 Subject: [PATCH] RAID (0/1/4/5/6/10/linear, mdadm 0.90 e 1.x, degradado) e LVM2 remontados no disco virtual Co-Authored-By: Claude Sonnet 5.5 --- crates/guara-core/src/assemble.rs | 196 +++++++- crates/guara-core/src/lib.rs | 2 + crates/guara-core/src/lvm.rs | 508 ++++++++++++++++++++ crates/guara-core/src/raid.rs | 690 ++++++++++++++++++++++++++++ crates/guara-core/src/scan.rs | 2 +- crates/guara-core/src/service.rs | 1 + crates/guara-core/src/types.rs | 4 + crates/guara-core/tests/e2e_raid.rs | 123 +++++ crates/guara-core/tests/lvm.rs | 31 ++ crates/guara-core/tests/raid.rs | 222 +++++++++ scripts/fetch-testdata.sh | 4 + 11 files changed, 1777 insertions(+), 6 deletions(-) create mode 100644 crates/guara-core/src/lvm.rs create mode 100644 crates/guara-core/src/raid.rs create mode 100644 crates/guara-core/tests/e2e_raid.rs create mode 100644 crates/guara-core/tests/lvm.rs create mode 100644 crates/guara-core/tests/raid.rs diff --git a/crates/guara-core/src/assemble.rs b/crates/guara-core/src/assemble.rs index e1b209f..777011a 100644 --- a/crates/guara-core/src/assemble.rs +++ b/crates/guara-core/src/assemble.rs @@ -75,6 +75,8 @@ pub struct Assembled { pub keys: Vec, /// Volumes lógicos / partições extras que não estão na tabela do disco. pub extra_parts: Vec, + /// Partições reais que são só contêineres (membros de RAID/LVM já remontados). + pub covered: Vec, pub notes: Vec, } @@ -119,6 +121,43 @@ pub fn assemble(path: &str, asm: &Assembly) -> Result { assemble_source(base, asm) } +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() }; + let wrapped = Overlay::new(r.src.clone(), vec![inner]); + if start == r.start { + r.len = r.len.max(len); + } + r.src = Arc::new(wrapped); + return; + } + regions.push(Region { start, len, src, label }); +} + +/// Candidato a membro de RAID/LVM: uma janela de disco (inteiro ou partição). +struct Window { + src: Arc, + /// Início no espaço do disco virtual, se a janela faz parte dele. + primary_start: Option, +} + +fn windows_of(src: &Arc, primary: bool) -> Vec { + 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 }; + v.push(Window { + src: Arc::new(SubSource::new(src.clone(), p.start, len)), + primary_start: primary.then_some(p.start), + }); + } + v +} + pub fn assemble_source(base: Arc, asm: &Assembly) -> Result { let secrets = to_secrets(asm)?; let mut regions: Vec = vec![]; @@ -128,16 +167,120 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result> = vec![]; + for m in &asm.members { + members.push(open_path(Path::new(m)).map_err(|e| Error::msg(format!("{m}: {e}")))?); + } + let mut next_virtual = base.size().div_ceil(MIB) * MIB + MIB; + let mut done_containers: std::collections::BTreeSet = Default::default(); + let mut tried_pool = false; + // Rodadas: o que foi aberto/montado pode conter outro volume cifrado, RAID ou LVM. + for round in 0..4 { let current: Arc = Arc::new(Overlay::new(base.clone(), regions.clone())); let layout = partition::analyze(current.as_ref(), false); + let mut all_parts: Vec = layout.partitions.clone(); + for p in &out.extra_parts { + if !all_parts.iter().any(|q| q.start == p.start) { + all_parts.push(p.clone()); + } + } let mut progress = false; + + // 1) RAID e LVM: reúne janelas do disco principal e dos membros extras. + if round == 0 || !tried_pool || !out.extra_parts.is_empty() { + let mut pool: Vec = vec![]; + 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) }); + } + } + for m in &members { + pool.extend(windows_of(m, false)); + } + tried_pool = true; + // RAID + let mut md: Vec<(Arc, crate::raid::MdSuper)> = vec![]; + let mut md_primary: Vec = vec![]; + for w in &pool { + if let Some(sup) = crate::raid::read_md_super(w.src.as_ref()) { + if let Some(st) = w.primary_start { + md_primary.push(st); + } + md.push((w.src.clone(), sup)); + } + } + 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))); + } + for res in crate::raid::assemble_md(new_md) { + match res { + Ok(a) => { + 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)); + } else { + out.notes.push(format!("{label} montado.")); + } + 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}")), + } + } + out.covered.extend(md_primary); + } + // LVM + let mut pvs: Vec<(Arc, 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(); + for (r, s) in regions.iter().zip(md_srcs) { + lvm_pool.push((s, Some(r.start))); + } + for (src, st) in lvm_pool { + if let Some(pv) = crate::lvm::read_pv(src.as_ref()) { + if !done_containers.insert(format!("pv{}", pv.uuid)) { + continue; + } + if let Some(st) = st { + pv_primary.push(st); + } + pvs.push((src, pv)); + } + } + if !pvs.is_empty() { + for vg in crate::lvm::assemble_vgs(pvs) { + for w in &vg.warnings { + out.notes.push(format!("LVM {}: {w}", vg.name)); + } + for (name, lv) in vg.lvs { + add_virtual(&mut regions, &mut next_virtual, &mut out, lv, format!("LVM {}/{}", vg.name, name)); + progress = true; + } + } + out.covered.extend(pv_primary); + } + } + + // 2) Desbloqueio de volumes cifrados. + let current: Arc = Arc::new(Overlay::new(base.clone(), regions.clone())); + let layout = partition::analyze(current.as_ref(), false); + let mut all_parts: Vec = layout.partitions.clone(); + for p in &out.extra_parts { + if !all_parts.iter().any(|q| q.start == p.start) { + all_parts.push(p.clone()); + } + } out.locked.clear(); - for p in &layout.partitions { - if p.encrypted.is_none() || regions.iter().any(|r| r.start == p.start) { + for p in &all_parts { + if p.encrypted.is_none() || out.unlocked.iter().any(|u| u.start == p.start) { continue; } let len = if p.len == 0 { current.size().saturating_sub(p.start) } else { p.len }; @@ -160,7 +303,7 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result out.locked.push(LockedInfo { @@ -179,6 +322,49 @@ pub fn assemble_source(base: Arc, asm: &Assembly) -> Result, next: &mut u64, out: &mut Assembled, src: Arc, label: String) { + let start = *next; + let len = src.size(); + if len == 0 { + return; + } + *next = (start + len).div_ceil(MIB) * MIB + MIB; + let inner = partition::analyze(src.as_ref(), false); + if inner.partitions.is_empty() { + out.extra_parts.push(PartInfo { + index: 0, + start, + len, + fs: FsKind::Unknown, + label: Some(label.clone()), + lost: false, + encrypted: None, + table_type: Some(label.clone()), + }); + } else { + for p in inner.partitions { + let mut q = p.clone(); + q.start += start; + q.label = q.label.or_else(|| Some(label.clone())); + out.extra_parts.push(q); + } + } + regions.push(Region { start, len, src, label }); +} diff --git a/crates/guara-core/src/lib.rs b/crates/guara-core/src/lib.rs index 921b9c0..662c5a4 100644 --- a/crates/guara-core/src/lib.rs +++ b/crates/guara-core/src/lib.rs @@ -16,11 +16,13 @@ pub mod hashing; pub mod health; pub mod http; pub mod image; +pub mod lvm; pub mod meta; pub mod notify; pub mod owner; pub mod partition; pub mod reconnect; +pub mod raid; 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 new file mode 100644 index 0000000..77cbc16 --- /dev/null +++ b/crates/guara-core/src/lvm.rs @@ -0,0 +1,508 @@ +//! LVM2: lê o rótulo do volume físico (PV) e o texto de metadados, e remonta os volumes +//! lógicos (lineares, em faixas, espelhados e RAID 0/1/5/6/10 do LVM). + +use crate::raid::{Layout, Level, Member, RaidParams, RaidSource}; +use crate::source::{read_vec, Source}; +use std::collections::BTreeMap; +use std::io; +use std::sync::Arc; + +const MDA_MAGIC: &[u8; 16] = b" LVM2 x[5A%r0N*>"; + +#[derive(Clone, Debug)] +pub struct PvInfo { + /// UUID de 32 caracteres, sem hífens. + pub uuid: String, + pub size: u64, + pub seqno: u64, + pub vg_name: String, + pub text: String, +} + +/// Lê rótulo + metadados de um PV. +pub fn read_pv(src: &dyn Source) -> Option { + let head = read_vec(src, 0, 2048).ok()?; + if head.len() < 2048 { + return None; + } + let sector = (0..4).find(|&s| &head[s * 512..s * 512 + 8] == b"LABELONE")?; + let lab = &head[sector * 512..(sector + 1) * 512]; + if &lab[24..32] != b"LVM2 001" { + return None; + } + let off = u32::from_le_bytes(lab[20..24].try_into().ok()?) as usize; + let pv = lab.get(off..)?; + let uuid = String::from_utf8_lossy(pv.get(..32)?).to_string(); + let size = u64::from_le_bytes(pv.get(32..40)?.try_into().ok()?); + // Lista de áreas de dados, depois de áreas de metadados; cada lista termina em (0,0). + let mut o = 40; + let rd = |o: usize| -> Option<(u64, u64)> { + Some(( + u64::from_le_bytes(pv.get(o..o + 8)?.try_into().ok()?), + u64::from_le_bytes(pv.get(o + 8..o + 16)?.try_into().ok()?), + )) + }; + loop { + let (a, b) = rd(o)?; + o += 16; + if a == 0 && b == 0 { + break; + } + } + let mut mdas = vec![]; + loop { + let (a, b) = rd(o)?; + o += 16; + if a == 0 && b == 0 { + break; + } + mdas.push((a, b)); + } + let mut best: Option = None; + for (mda_off, mda_size) in mdas { + let Ok(h) = read_vec(src, mda_off, 512) else { continue }; + if h.len() < 512 || &h[4..20] != MDA_MAGIC { + continue; + } + // raw_locn[0] + let loc_off = u64::from_le_bytes(h[40..48].try_into().unwrap()); + let loc_size = u64::from_le_bytes(h[48..56].try_into().unwrap()); + if loc_size == 0 || loc_size > 16 << 20 || mda_size < 1024 { + continue; + } + let mut text = vec![]; + if loc_off + loc_size <= mda_size { + if let Ok(t) = read_vec(src, mda_off + loc_off, loc_size as usize) { + text = t; + } + } else { + // O texto dá a volta no anel. + let first = mda_size - loc_off; + if let (Ok(a), Ok(b)) = ( + read_vec(src, mda_off + loc_off, first as usize), + read_vec(src, mda_off + 512, (loc_size - first) as usize), + ) { + text = [a, b].concat(); + } + } + 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 seqno = vg.int("seqno").unwrap_or(0); + 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); + } + } + best +} + +// ------------------------------------------------------------------ texto + +#[derive(Clone, Debug)] +pub enum Val { + Int(i64), + Str(String), + List(Vec), +} + +#[derive(Clone, Debug, Default)] +pub struct Section { + pub values: BTreeMap, + /// Subseções na ordem do arquivo. + pub sections: Vec<(String, Section)>, +} + +impl Section { + pub fn int(&self, k: &str) -> Option { + match self.values.get(k)? { + Val::Int(i) => Some(*i as u64), + _ => None, + } + } + pub fn str(&self, k: &str) -> Option<&str> { + match self.values.get(k)? { + Val::Str(s) => Some(s), + _ => None, + } + } + pub fn list(&self, k: &str) -> Option<&[Val]> { + match self.values.get(k)? { + Val::List(l) => Some(l), + _ => None, + } + } + pub fn sub(&self, name: &str) -> Option<&Section> { + self.sections.iter().find(|(n, _)| n == name).map(|(_, s)| s) + } +} + +struct Lexer<'a> { + b: &'a [u8], + i: usize, +} + +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',') { + self.i += 1; + } + if self.i < self.b.len() && self.b[self.i] == b'#' { + while self.i < self.b.len() && self.b[self.i] != b'\n' { + self.i += 1; + } + } else { + break; + } + } + } + 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'#') { + self.i += 1; + } + (self.i > s).then(|| String::from_utf8_lossy(&self.b[s..self.i]).to_string()) + } + fn value(&mut self) -> Option { + self.skip(); + match *self.b.get(self.i)? { + b'"' => { + self.i += 1; + let s = self.i; + while self.i < self.b.len() && self.b[self.i] != b'"' { + if self.b[self.i] == b'\\' { + self.i += 1; + } + self.i += 1; + } + let v = String::from_utf8_lossy(&self.b[s..self.i.min(self.b.len())]).to_string(); + self.i += 1; + Some(Val::Str(v)) + } + b'[' => { + self.i += 1; + let mut l = vec![]; + loop { + self.skip(); + if *self.b.get(self.i)? == b']' { + self.i += 1; + break; + } + l.push(self.value()?); + } + Some(Val::List(l)) + } + _ => { + let s = self.i; + 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) + } + } + } + fn section(&mut self, top: bool) -> Option
{ + let mut sec = Section::default(); + loop { + self.skip(); + if self.i >= self.b.len() { + return top.then_some(sec); + } + if self.b[self.i] == b'}' { + self.i += 1; + return (!top).then_some(sec); + } + let name = self.ident()?; + self.skip(); + match *self.b.get(self.i)? { + b'=' => { + self.i += 1; + let v = self.value()?; + sec.values.insert(name, v); + } + b'{' => { + self.i += 1; + let s = self.section(false)?; + sec.sections.push((name, s)); + } + _ => return None, + } + } + } +} + +pub fn parse(text: &str) -> Option
{ + Lexer { b: text.as_bytes(), i: 0 }.section(true) +} + +// ------------------------------------------------------------- montagem + +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]) +} + +#[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)> }, + Raid(Arc), +} + +struct Seg { + start: u64, + len: u64, + map: Map, +} + +pub struct LvSource { + segs: Vec, + size: u64, +} + +impl Source for LvSource { + fn size(&self) -> u64 { + self.size + } + fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result { + if offset >= self.size || buf.is_empty() { + return Ok(0); + } + let want = buf.len().min((self.size - offset) as usize); + let mut done = 0; + while done < want { + let pos = offset + done as u64; + let i = self.segs.partition_point(|s| s.start + s.len <= pos); + let Some(s) = self.segs.get(i) else { break }; + let rel = pos - s.start; + let mut n = ((s.len - rel) as usize).min(want - done); + let dst = &mut buf[done..done + n]; + match &s.map { + Map::Linear { src, base } => match src { + Some(src) => n = read_full(src.as_ref(), base + rel, dst)?, + None => dst.fill(0), + }, + Map::Striped { srcs, stripe } => { + let ns = srcs.len() as u64; + let (idx, within) = (rel / stripe, rel % stripe); + n = n.min((stripe - within) as usize); + let (src, base) = &srcs[(idx % ns) as usize]; + let off = base + (idx / ns) * stripe + within; + match src { + Some(src) => n = read_full(src.as_ref(), off, &mut dst[..n])?, + None => dst[..n].fill(0), + } + } + Map::Mirror { legs } => { + let mut ok = false; + for (src, base) in legs { + if let Some(src) = src { + if let Ok(g) = read_full(src.as_ref(), base + rel, dst) { + n = g; + ok = true; + break; + } + } + } + if !ok { + dst.fill(0); + } + } + Map::Raid(r) => n = read_full(r.as_ref(), rel, dst)?, + } + if n == 0 { + break; + } + done += n; + } + Ok(done) + } +} + +fn read_full(src: &dyn Source, off: u64, buf: &mut [u8]) -> io::Result { + let mut got = 0; + while got < buf.len() { + let n = src.read_at(off + got as u64, &mut buf[got..])?; + if n == 0 { + buf[got..].fill(0); + return Ok(buf.len()); + } + got += n; + } + Ok(got) +} + +pub struct VgResult { + pub name: String, + pub lvs: Vec<(String, Arc)>, + pub warnings: Vec, +} + +fn mk_stripes(list: &[Val]) -> Vec<(String, u64)> { + let mut out = vec![]; + for pair in list.chunks(2) { + if let [Val::Str(n), Val::Int(e)] = pair { + out.push((n.clone(), *e as u64)); + } + } + out +} + +struct Ctx<'a> { + ext: u64, + pvs: BTreeMap>, u64)>, // nome -> (origem, pe_start em bytes) + lvs: &'a Section, + cache: BTreeMap>>, + warnings: Vec, +} + +impl Ctx<'_> { + fn build(&mut self, name: &str, depth: u32) -> Option> { + if let Some(c) = self.cache.get(name) { + return c.clone(); + } + if depth > 6 { + return None; + } + let lv = self.lvs.sub(name)?.clone(); + let nsegs = lv.int("segment_count").unwrap_or(0); + let mut segs: Vec = vec![]; + for i in 1..=nsegs { + let Some(seg) = lv.sub(&format!("segment{i}")) else { + self.warnings.push(format!("{name}: segmento {i} ausente")); + return None; + }; + let start = seg.int("start_extent").unwrap_or(0) * self.ext; + let len = seg.int("extent_count").unwrap_or(0) * self.ext; + let ty = seg.str("type").unwrap_or("").to_string(); + let map = match ty.as_str() { + "striped" => { + let st = mk_stripes(seg.list("stripes").unwrap_or(&[])); + let srcs: Vec<(Option>, u64)> = st + .iter() + .map(|(pv, e)| match self.pvs.get(pv) { + Some((s, pe)) => (s.clone(), pe + e * self.ext), + None => (None, 0), + }) + .collect(); + if srcs.iter().any(|(s, _)| s.is_none()) { + 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 } + } + } + "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))) + .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(); + // Pares (rimage, rmeta) ou lista só de imagens. + 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 })); + } + let chunk = seg.int("stripe_size").unwrap_or(128) * 512; + let (level, layout) = match t { + "raid1" => (Level::R1, Layout::LeftSymmetric), + "raid0" | "raid0_meta" => (Level::R0, Layout::LeftSymmetric), + "raid4" | "raid5_n" => (Level::R4, Layout::ParityLast), + "raid5" | "raid5_ls" => (Level::R5, Layout::LeftSymmetric), + "raid5_la" => (Level::R5, Layout::LeftAsymmetric), + "raid5_rs" => (Level::R5, Layout::RightSymmetric), + "raid5_ra" => (Level::R5, Layout::RightAsymmetric), + "raid6" | "raid6_zr" | "raid6_ls_6" => (Level::R6, Layout::LeftSymmetric), + "raid6_nr" => (Level::R6, Layout::LeftAsymmetric), + "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")); + return None; + } + }; + let mut p = RaidParams::simple(level, chunk.max(512), layout, members); + p.near = 2; + p.far = 1; + match RaidSource::new(p) { + Ok(r) => Map::Raid(Arc::new(r)), + Err(e) => { + self.warnings.push(format!("{name}: {e}")); + return None; + } + } + } + other => { + self.warnings.push(format!("{name}: tipo de segmento \"{other}\" não suportado")); + return None; + } + }; + segs.push(Seg { start, len, map }); + } + segs.sort_by_key(|s| s.start); + let size = segs.iter().map(|s| s.start + s.len).max().unwrap_or(0); + let src: Arc = Arc::new(LvSource { segs, size }); + self.cache.insert(name.to_string(), Some(src.clone())); + Some(src) + } +} + +/// Monta os volumes lógicos de cada grupo de volumes presente nos PVs fornecidos. +pub fn assemble_vgs(pvs: Vec<(Arc, PvInfo)>) -> Vec { + let mut by_vg: BTreeMap, PvInfo)>> = BTreeMap::new(); + for (s, p) in pvs { + by_vg.entry(p.vg_name.clone()).or_default().push((s, p)); + } + 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 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()); + 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)); + } + } + 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); + if hidden { + continue; + } + if let Some(src) = ctx.build(name, 0) { + lvs.push((name.clone(), src)); + } + } + out.push(VgResult { name: vg_name, lvs, warnings: ctx.warnings }); + } + out +} diff --git a/crates/guara-core/src/raid.rs b/crates/guara-core/src/raid.rs new file mode 100644 index 0000000..09798d4 --- /dev/null +++ b/crates/guara-core/src/raid.rs @@ -0,0 +1,690 @@ +//! RAID por software: linear, 0, 1, 4, 5, 6 e 10, a partir dos discos soltos. +//! +//! Lê os metadados do mdadm (superbloco 0.90 e 1.x) para montar sozinho; sem metadados, o +//! módulo `raid_detect` descobre ordem, tamanho de faixa e layout. Se faltarem discos, o +//! array é lido em modo degradado (paridade reconstrói o que faltar). + +use crate::source::{read_vec, Source}; +use std::io; +use std::sync::Arc; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Level { + Linear, + R0, + R1, + R4, + R5, + R6, + R10, +} + +impl Level { + pub fn name(self) -> &'static str { + match self { + Level::Linear => "linear", + Level::R0 => "RAID 0", + Level::R1 => "RAID 1", + Level::R4 => "RAID 4", + Level::R5 => "RAID 5", + Level::R6 => "RAID 6", + Level::R10 => "RAID 10", + } + } +} + +/// Posição da paridade e ordem dos dados, como no driver md do Linux. +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Layout { + LeftAsymmetric, + RightAsymmetric, + LeftSymmetric, + RightSymmetric, + /// RAID 4 e variantes "parity-0"/"parity-n". + ParityFirst, + ParityLast, +} + +impl Layout { + pub fn from_md(v: u32) -> Layout { + match v { + 0 => Layout::LeftAsymmetric, + 1 => Layout::RightAsymmetric, + 2 => Layout::LeftSymmetric, + 3 => Layout::RightSymmetric, + 4 => Layout::ParityFirst, + 5 => Layout::ParityLast, + _ => Layout::LeftSymmetric, + } + } + pub fn all_raid5() -> [Layout; 4] { + [Layout::LeftSymmetric, Layout::LeftAsymmetric, Layout::RightSymmetric, Layout::RightAsymmetric] + } +} + +#[derive(Clone)] +pub struct Member { + pub src: Arc, + /// Onde começam os dados dentro do membro. + pub data_offset: u64, +} + +#[derive(Clone)] +pub struct RaidParams { + pub level: Level, + /// Tamanho da faixa por disco, em bytes. + pub chunk: u64, + pub layout: Layout, + /// Membros na ordem do array; `None` = disco ausente. + pub members: Vec>, + /// RAID 10: cópias "perto" (near) e "longe" (far). + pub near: u32, + pub far: u32, + pub far_offset: bool, + /// Bytes úteis por membro (0 = até o fim do menor). + pub per_member: u64, +} + +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 } + } +} + +pub struct RaidSource { + p: RaidParams, + size: u64, + per_member: u64, +} + +// ------------------------------------------------------------ GF(2^8) para RAID 6 + +fn gf_mul(mut a: u8, mut b: u8) -> u8 { + let mut r = 0u8; + while b != 0 { + if b & 1 != 0 { + r ^= a; + } + let hi = a & 0x80; + a <<= 1; + if hi != 0 { + a ^= 0x1d; + } + b >>= 1; + } + r +} + +fn gf_pow2(n: usize) -> u8 { + let mut r = 1u8; + for _ in 0..n % 255 { + r = gf_mul(r, 2); + } + r +} + +fn gf_inv(a: u8) -> u8 { + // a^254 + let mut r = 1u8; + let mut base = a; + let mut e = 254u32; + while e > 0 { + if e & 1 != 0 { + r = gf_mul(r, base); + } + base = gf_mul(base, base); + e >>= 1; + } + r +} + +impl RaidSource { + pub fn new(p: RaidParams) -> Result { + let n = p.members.len(); + if n == 0 { + return Err("RAID sem membros".into()); + } + 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 }; + if p.level != Level::Linear { + per_member = per_member / p.chunk.max(512) * p.chunk.max(512); + } + let missing = n - present.len(); + let need = match p.level { + Level::Linear | Level::R0 => 0, + Level::R1 => n - 1, + Level::R4 | Level::R5 => 1, + Level::R6 => 2, + 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())); + } + 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()); + } + if p.level == Level::R10 && (n < 2 || p.near as usize * p.far as usize > n.max(2)) { + 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::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 }) + } + + pub fn params(&self) -> &RaidParams { + &self.p + } + + 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 mut done = 0; + while done < buf.len() { + let n = m.src.read_at(m.data_offset + off + done as u64, &mut buf[done..])?; + if n == 0 { + buf[done..].fill(0); + break; + } + done += n; + } + Ok(()) + } + + /// (disco de paridade P, disco de paridade Q, disco que guarda o dado `d` da linha `row`) + fn raid5_map(&self, row: u64, d: usize) -> (usize, usize) { + let n = self.p.members.len(); + let r = (row % n as u64) as usize; + 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 }; + (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 }; + (pd, disk) + } + Layout::ParityFirst => (0, d + 1), + Layout::ParityLast => (n - 1, d), + } + } + + fn raid6_map(&self, row: u64, d: usize) -> (usize, usize, usize) { + let n = self.p.members.len(); + let r = (row % n as u64) as usize; + match self.p.layout { + Layout::LeftAsymmetric | Layout::LeftSymmetric => { + let pd = n - 1 - r; + let qd = (pd + 1) % n; + let disk = if self.p.layout == Layout::LeftSymmetric { + (pd + 2 + d) % n + } else { + // assimétrico: dados em ordem pulando P e Q + let mut k = d; + for skip in sorted2(pd, qd) { + if k >= skip { + k += 1; + } + } + k + }; + (pd, qd, disk) + } + Layout::RightAsymmetric | Layout::RightSymmetric => { + let pd = r; + let qd = (pd + 1) % n; + let disk = if self.p.layout == Layout::RightSymmetric { + (pd + 2 + d) % n + } else { + let mut k = d; + for skip in sorted2(pd, qd) { + if k >= skip { + k += 1; + } + } + k + }; + (pd, qd, disk) + } + Layout::ParityFirst => (0, 1, d + 2), + Layout::ParityLast => (n - 2, n - 1, d), + } + } + + /// Lê `buf.len()` bytes dentro de um único chunk lógico. + fn read_in_chunk(&self, chunk_idx: u64, within: u64, buf: &mut [u8]) -> io::Result<()> { + let n = self.p.members.len(); + let c = self.p.chunk; + match self.p.level { + Level::Linear => unreachable!(), + Level::R0 => { + let (row, disk) = (chunk_idx / n as u64, (chunk_idx % n as u64) as usize); + self.read_member(disk, row * c + within, buf) + } + 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() { + return Ok(()); + } + } + Err(io::Error::new(io::ErrorKind::Other, "nenhuma cópia legível")) + } + Level::R4 | Level::R5 => { + let dd = n - 1; + let (row, d) = (chunk_idx / dd as u64, (chunk_idx % dd as u64) as usize); + let (pd, disk) = self.raid5_map(row, d); + let off = row * c + within; + if self.p.members[disk].is_some() { + return self.read_member(disk, off, buf); + } + // Reconstrói por XOR de todos os outros (dados + paridade) da linha. + buf.fill(0); + let mut tmp = vec![0u8; buf.len()]; + for i in 0..n { + if i == disk { + continue; + } + let _ = pd; + self.read_member(i, off, &mut tmp)?; + for (a, b) in buf.iter_mut().zip(&tmp) { + *a ^= *b; + } + } + Ok(()) + } + Level::R6 => { + let dd = n - 2; + let (row, d) = (chunk_idx / dd as u64, (chunk_idx % dd as u64) as usize); + let (_, _, disk) = self.raid6_map(row, d); + let off = row * c + within; + if self.p.members[disk].is_some() { + return self.read_member(disk, off, buf); + } + self.raid6_rebuild(row, d, off, buf) + } + Level::R10 => { + let mut last = None; + for (disk, off) in self.r10_locations(chunk_idx) { + if self.p.members[disk].is_some() { + match self.read_member(disk, off + within, buf) { + Ok(()) => return Ok(()), + Err(e) => last = Some(e), + } + } + } + Err(last.unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "cópias ausentes"))) + } + } + } + + /// Onde ficam todas as cópias de um chunk lógico (como `raid10_find_phys` do kernel). + fn r10_locations(&self, chunk_idx: u64) -> Vec<(usize, u64)> { + let n = self.p.members.len() as u64; + let c = self.p.chunk; + let near = self.p.near.max(1) as u64; + let far = self.p.far.max(1) as u64; + let slot0 = chunk_idx * near; + let (mut stripe, dev) = (slot0 / n, slot0 % n); + if self.p.far_offset { + stripe *= far; + } + 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; + let mut firsts = vec![]; + for _ in 0..near { + firsts.push((d, off)); + out.push((d as usize, off)); + d += 1; + if d >= n { + d = 0; + off += c; + } + } + for f in 1..far { + for &(dv, of) in &firsts { + out.push((((dv + near * f) % n) as usize, of + stride * f)); + } + } + out + } + + fn raid6_rebuild(&self, row: u64, d: usize, off: u64, buf: &mut [u8]) -> io::Result<()> { + let n = self.p.members.len(); + let dd = n - 2; + let (pd, qd, _) = self.raid6_map(row, 0); + 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 p_ok = self.p.members[pd].is_some(); + let q_ok = self.p.members[qd].is_some(); + let mut data: Vec>> = vec![None; dd]; + for k in 0..dd { + if self.p.members[disks[k]].is_some() { + let mut v = vec![0u8; len]; + self.read_member(disks[k], off, &mut v)?; + data[k] = Some(v); + } + } + let rd = |disk: usize| -> io::Result> { + let mut v = vec![0u8; len]; + self.read_member(disk, off, &mut v)?; + Ok(v) + }; + if missing_data.len() == 1 && p_ok { + let p = rd(pd)?; + let mut acc = p; + for v in data.iter().flatten() { + for (a, b) in acc.iter_mut().zip(v) { + *a ^= *b; + } + } + buf.copy_from_slice(&acc); + return Ok(()); + } + if missing_data.len() == 1 && q_ok { + let q = rd(qd)?; + let x = missing_data[0]; + let mut acc = q; + for (k, v) in data.iter().enumerate() { + if let Some(v) = v { + let g = gf_pow2(k); + for (a, b) in acc.iter_mut().zip(v) { + *a ^= gf_mul(g, *b); + } + } + } + let inv = gf_inv(gf_pow2(x)); + for (o, a) in buf.iter_mut().zip(&acc) { + *o = gf_mul(inv, *a); + } + return Ok(()); + } + if missing_data.len() == 2 && p_ok && q_ok { + let (x, y) = (missing_data[0], missing_data[1]); + let p = rd(pd)?; + let q = rd(qd)?; + let (mut pxy, mut qxy) = (p, q); + for (k, v) in data.iter().enumerate() { + if let Some(v) = v { + let g = gf_pow2(k); + for i in 0..len { + pxy[i] ^= v[i]; + qxy[i] ^= gf_mul(g, v[i]); + } + } + } + // Dx ^ Dy = pxy ; g^x Dx ^ g^y Dy = qxy + let (gx, gy) = (gf_pow2(x), gf_pow2(y)); + let denom_inv = gf_inv(gx ^ gy); + let target = if d == x { x } else { y }; + for i in 0..len { + // Dx = (qxy ^ gy*pxy) / (gx ^ gy) + let dx = gf_mul(denom_inv, qxy[i] ^ gf_mul(gy, pxy[i])); + buf[i] = if target == x { dx } else { pxy[i] ^ dx }; + } + return Ok(()); + } + Err(io::Error::new(io::ErrorKind::Other, "RAID 6 sem redundância suficiente nesta linha")) + } +} + +fn sorted2(a: usize, b: usize) -> [usize; 2] { + if a < b { + [a, b] + } else { + [b, a] + } +} + +impl Source for RaidSource { + fn size(&self) -> u64 { + self.size + } + + fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result { + if offset >= self.size || buf.is_empty() { + return Ok(0); + } + let want = buf.len().min((self.size - offset) as usize); + let mut done = 0usize; + if self.p.level == Level::Linear { + let mut base = 0u64; + for m in self.p.members.iter().flatten() { + let len = m.src.size().saturating_sub(m.data_offset); + 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])?; + if got == 0 { + break; + } + done += got; + if done >= want { + break; + } + } + base += len; + } + return Ok(done); + } + let c = self.p.chunk; + while done < want { + let pos = offset + done as u64; + let (idx, within) = (pos / c, pos % c); + let n = ((c - within) as usize).min(want - done); + self.read_in_chunk(idx, within, &mut buf[done..done + n])?; + done += n; + } + Ok(done) + } +} + +// ============================================================ metadados do mdadm + +#[derive(Clone, Debug)] +pub struct MdSuper { + pub version: String, + pub uuid: [u8; 16], + pub name: String, + pub level: Level, + pub layout: u32, + pub chunk: u64, + pub raid_disks: u32, + /// Papel deste disco no array (None = reserva/falho). + pub role: Option, + pub data_offset: u64, + /// Bytes de dados por membro. + pub data_size: u64, + pub events: u64, + pub array_size: u64, +} + +fn le32(b: &[u8], o: usize) -> u32 { + u32::from_le_bytes(b[o..o + 4].try_into().unwrap()) +} +fn le64(b: &[u8], o: usize) -> u64 { + u64::from_le_bytes(b[o..o + 8].try_into().unwrap()) +} + +fn level_from(v: i32) -> Option { + Some(match v { + -1 => Level::Linear, + 0 => Level::R0, + 1 => Level::R1, + 4 => Level::R4, + 5 => Level::R5, + 6 => Level::R6, + 10 => Level::R10, + _ => return None, + }) +} + +/// Procura um superbloco md nas posições possíveis de `src` (um disco ou partição). +pub fn read_md_super(src: &dyn Source) -> Option { + let size = src.size(); + // 1.1 no início, 1.2 a 4 KiB, 1.0 a 8 KiB do fim. + let mut cands: Vec<(u64, &str)> = vec![(0, "1.1"), (4096, "1.2")]; + if size > 16384 { + cands.push(((size & !4095) - 8192, "1.0")); + } + for (off, ver) in cands { + let Ok(b) = read_vec(src, off, 4096) else { continue }; + if b.len() < 4096 || le32(&b, 0) != 0xA92B_4EFC || le32(&b, 4) != 1 { + continue; + } + let level = level_from(le32(&b, 72) as i32)?; + let max_dev = le32(&b, 220) as usize; + let dev_number = le32(&b, 160) as usize; + let role = if dev_number < max_dev.min(1024) && 256 + dev_number * 2 + 2 <= b.len() { + let r = u16::from_le_bytes([b[256 + dev_number * 2], b[256 + dev_number * 2 + 1]]); + (r < 0xFFFE).then_some(r as u32) + } else { + None + }; + 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 data_offset = le64(&b, 128) * 512; + let data_size = le64(&b, 136) * 512; + let raid_disks = le32(&b, 92); + let chunk = le32(&b, 88) as u64 * 512; + let used = le64(&b, 80) * 512; + let array_size = match level { + Level::R0 => used * raid_disks as u64, + Level::R5 | Level::R4 => used * (raid_disks as u64).saturating_sub(1), + Level::R6 => used * (raid_disks as u64).saturating_sub(2), + _ => used, + }; + return Some(MdSuper { + version: ver.into(), + uuid, + name, + level, + layout: le32(&b, 76), + chunk, + raid_disks, + role, + data_offset, + data_size, + events: le64(&b, 200), + array_size, + }); + } + // 0.90: no fim, alinhado a 64 KiB. + if size > 131072 { + let off = (size & !65535) - 65536; + if let Ok(b) = read_vec(src, off, 4096) { + if b.len() >= 1024 && le32(&b, 0) == 0xA92B_4EFC && le32(&b, 4) == 0 { + let level = level_from(le32(&b, 28) as i32)?; + let mut uuid = [0u8; 16]; + uuid[0..4].copy_from_slice(&b[20..24]); + uuid[4..16].copy_from_slice(&b[52..64]); + let role = { + let r = le32(&b, 768 + 12); + (r < 0xFFFE).then_some(r) + }; + let raid_disks = le32(&b, 40); + let used = le32(&b, 32) as u64 * 1024; + return Some(MdSuper { + version: "0.90".into(), + uuid, + name: String::new(), + level, + layout: le32(&b, 256), + chunk: le32(&b, 260) as u64, + raid_disks, + role, + data_offset: 0, + data_size: used, + events: ((le32(&b, 39 * 4) as u64) << 32) | le32(&b, 40 * 4) as u64, + array_size: match level { + Level::R0 => used * raid_disks as u64, + Level::R5 | Level::R4 => used * (raid_disks as u64).saturating_sub(1), + Level::R6 => used * (raid_disks as u64).saturating_sub(2), + _ => used, + }, + }); + } + } + } + None +} + +fn uuid_str(u: &[u8; 16]) -> String { + u.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Um array reconhecido pelos superblocos dos discos fornecidos. +pub struct FoundArray { + pub uuid: String, + pub name: String, + pub info: MdSuper, + pub source: Arc, + pub missing: usize, +} + +/// Agrupa membros por UUID e monta cada array (degradado se for possível). +pub fn assemble_md(members: Vec<(Arc, MdSuper)>) -> Vec> { + let mut groups: Vec<(String, Vec<(Arc, MdSuper)>)> = vec![]; + for (s, m) in members { + let key = uuid_str(&m.uuid); + match groups.iter_mut().find(|g| g.0 == key) { + Some(g) => g.1.push((s, m)), + None => groups.push((key, vec![(s, m)])), + } + } + let mut out = vec![]; + for (uuid, mut list) in groups { + // O membro com mais eventos manda; membros desatualizados são tratados como ausentes. + list.sort_by_key(|(_, m)| std::cmp::Reverse(m.events)); + let top = list[0].1.clone(); + let n = top.raid_disks as usize; + let mut slots: Vec> = vec![None; n]; + let mut stale = 0; + for (s, m) in &list { + if let Some(r) = m.role { + if (r as usize) < n && slots[r as usize].is_none() { + if m.events + 1 < top.events && top.events > 0 { + stale += 1; + continue; + } + slots[r as usize] = Some(Member { src: s.clone(), data_offset: m.data_offset }); + } + } + } + let missing = slots.iter().filter(|s| s.is_none()).count(); + let mut p = RaidParams::simple(top.level, top.chunk.max(512), Layout::from_md(top.layout), slots); + if top.level == Level::R10 { + p.near = top.layout & 0xff; + p.far = (top.layout >> 8) & 0xff; + p.far_offset = top.layout & 0x10000 != 0; + } + if top.data_size > 0 { + p.per_member = top.data_size; + } + match RaidSource::new(p) { + Ok(src) => out.push(Ok(FoundArray { + uuid: uuid.clone(), + name: top.name.clone(), + info: top, + source: Arc::new(src), + missing: missing + stale, + })), + Err(e) => out.push(Err(format!("array {uuid}: {e}"))), + } + } + out +} diff --git a/crates/guara-core/src/scan.rs b/crates/guara-core/src/scan.rs index ad6f2c4..7d8fa51 100644 --- a/crates/guara-core/src/scan.rs +++ b/crates/guara-core/src/scan.rs @@ -298,7 +298,7 @@ fn build_ranges( let free = merge_extents(free.to_vec()); let mut out: Vec = vec![]; for p in &layout.partitions { - if p.encrypted.is_some() { + if p.encrypted.is_some() || opts.covered.contains(&p.start) { continue; } let whole = Extent::new(p.start, p.len.min(disk_size.saturating_sub(p.start))); diff --git a/crates/guara-core/src/service.rs b/crates/guara-core/src/service.rs index 79bc26f..75a7443 100644 --- a/crates/guara-core/src/service.rs +++ b/crates/guara-core/src/service.rs @@ -209,6 +209,7 @@ impl Engine { let fp = crate::source::fingerprint(raw.as_ref()); let asm = crate::assemble::assemble_source(raw, &opts.assembly)?; opts.extra_parts = asm.extra_parts.clone(); + opts.covered = asm.covered.clone(); if opts.assembly.remember_keys { opts.assembly.keys = asm.keys.clone(); } diff --git a/crates/guara-core/src/types.rs b/crates/guara-core/src/types.rs index ee19190..5c2ad5c 100644 --- a/crates/guara-core/src/types.rs +++ b/crates/guara-core/src/types.rs @@ -324,6 +324,9 @@ pub struct ScanOptions { /// Partições que não estão na tabela do disco (volumes lógicos, desbloqueados). #[serde(default)] pub extra_parts: Vec, + /// Partições que só contêm RAID/LVM remontado: a varredura entra pelos volumes virtuais. + #[serde(default)] + pub covered: Vec, } fn default_max_carve() -> u64 { @@ -339,6 +342,7 @@ impl Default for ScanOptions { max_carve_size: default_max_carve(), assembly: Default::default(), extra_parts: vec![], + covered: vec![], } } } diff --git a/crates/guara-core/tests/e2e_raid.rs b/crates/guara-core/tests/e2e_raid.rs new file mode 100644 index 0000000..7d15a9e --- /dev/null +++ b/crates/guara-core/tests/e2e_raid.rs @@ -0,0 +1,123 @@ +//! Ponta a ponta: RAID 5 de 4 discos (um perdido) com ext4 dentro e um arquivo apagado. + +mod common; +use common::*; +use guara_core::assemble::{assemble_source, Assembly}; +use guara_core::scan::{self, Outcome}; +use guara_core::source::{FileSource, Source}; +use guara_core::*; +use std::path::Path; +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) +} + +fn data(seed: u64, n: usize) -> Vec { + let mut x = seed | 1; + (0..n) + .map(|_| { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + (x >> 24) as u8 + }) + .collect() +} + +const CHUNK: usize = 64 * 1024; + +fn md_super(n: u32, dev: u32, data_off: u64, sectors: 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()); + b[16..32].fill(0x5a); + b[72..76].copy_from_slice(&5u32.to_le_bytes()); + b[76..80].copy_from_slice(&2u32.to_le_bytes()); + b[80..88].copy_from_slice(§ors.to_le_bytes()); + b[88..92].copy_from_slice(&((CHUNK / 512) as u32).to_le_bytes()); + b[92..96].copy_from_slice(&n.to_le_bytes()); + b[128..136].copy_from_slice(&data_off.to_le_bytes()); + b[136..144].copy_from_slice(§ors.to_le_bytes()); + b[160..164].copy_from_slice(&dev.to_le_bytes()); + b[200..208].copy_from_slice(&50u64.to_le_bytes()); + b[220..224].copy_from_slice(&n.to_le_bytes()); + for i in 0..n as usize { + b[256 + i * 2..258 + i * 2].copy_from_slice(&(i as u16).to_le_bytes()); + } + b +} + +#[test] +fn deleted_file_on_degraded_raid5() { + if !(have("mke2fs") && have("debugfs")) { + eprintln!("SKIP: e2fsprogs ausente"); + return; + } + init(); + let dir = tempfile::tempdir().unwrap(); + let root = dir.path().join("root"); + std::fs::create_dir_all(root.join("fotos")).unwrap(); + let doc = data(5, 300_000); + std::fs::write(root.join("fotos/ferias.dat"), &doc).unwrap(); + std::fs::write(root.join("fotos/outra.dat"), data(6, 9000)).unwrap(); + let plain = dir.path().join("plain.img"); + let n = 4usize; + 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(); + let logical = std::fs::read(&plain).unwrap(); + + // RAID 5 left-symmetric + let data_off = 2048u64; // setores + let mut disks = vec![vec![0u8; data_off as usize * 512 + rows * CHUNK]; n]; + for row in 0..rows { + let pd = n - 1 - row % n; + let mut p = vec![0u8; CHUNK]; + for d in 0..dd { + let disk = (pd + 1 + d) % n; + let src = &logical[(row * dd + d) * CHUNK..(row * dd + d + 1) * CHUNK]; + disks[disk][data_off as usize * 512 + row * CHUNK..][..CHUNK].copy_from_slice(src); + for i in 0..CHUNK { + p[i] ^= src[i]; + } + } + disks[pd][data_off as usize * 512 + row * CHUNK..][..CHUNK].copy_from_slice(&p); + } + let mut paths = vec![]; + for (i, d) in disks.iter_mut().enumerate() { + if i == 2 { + continue; // disco que morreu + } + let sb = md_super(n as u32, i as u32, data_off, (rows * CHUNK / 512) as u64); + d[4096..8192].copy_from_slice(&sb); + let p = dir.path().join(format!("disk{i}.img")); + std::fs::write(&p, &d).unwrap(); + paths.push(p); + } + 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(), + ..Default::default() + }; + let a = assemble_source(base, &asm).unwrap(); + 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 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::>())); + 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(); + 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) }; + assert!(std::fs::read(p).unwrap() == doc); +} diff --git a/crates/guara-core/tests/lvm.rs b/crates/guara-core/tests/lvm.rs new file mode 100644 index 0000000..4997c6e --- /dev/null +++ b/crates/guara-core/tests/lvm.rs @@ -0,0 +1,31 @@ +//! LVM2: imagem real do conjunto de testes do dfvfs (target/testdata/dfvfs/lvm.raw). + +use guara_core::lvm; +use guara_core::partition::detect_fs; +use guara_core::source::{FileSource, Source}; +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"); + if !p.exists() { + eprintln!("SKIP: execute scripts/fetch-testdata.sh"); + return; + } + let src: Arc = Arc::new(FileSource::open(&p).unwrap()); + let pv = lvm::read_pv(src.as_ref()).expect("PV"); + assert_eq!(pv.vg_name, "test_volume_group"); + let vgs = lvm::assemble_vgs(vec![(src, pv)]); + assert_eq!(vgs.len(), 1); + let vg = &vgs[0]; + assert!(vg.warnings.is_empty(), "{:?}", vg.warnings); + let names: Vec<&str> = vg.lvs.iter().map(|(n, _)| n.as_str()).collect(); + assert_eq!(names, ["test_logical_volume1", "test_logical_volume2"]); + for (n, lv) in &vg.lvs { + assert_eq!(lv.size(), 4 << 20, "{n}"); + let fs = detect_fs(lv.as_ref(), 0); + eprintln!("{n}: {fs:?}"); + // O segundo volume da imagem de referência foi criado e nunca formatado. + assert_eq!(fs.is_some(), n == "test_logical_volume1", "{n}"); + } +} diff --git a/crates/guara-core/tests/raid.rs b/crates/guara-core/tests/raid.rs new file mode 100644 index 0000000..7eaff67 --- /dev/null +++ b/crates/guara-core/tests/raid.rs @@ -0,0 +1,222 @@ +//! RAID por software: arrays gerados por codificadores independentes (faixa a faixa), +//! lidos de volta inteiros, degradados e com superblocos md 1.x e 0.90. + +use guara_core::raid::*; +use guara_core::source::{read_vec, MemSource, Source}; +use std::sync::Arc; + +const C: usize = 4096; + +fn data(len: usize) -> Vec { + let mut x = 0x1234_5678_9ABC_DEF1u64; + (0..len) + .map(|_| { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + (x >> 24) as u8 + }) + .collect() +} + +fn gmul(mut a: u8, mut b: u8) -> u8 { + let mut r = 0; + while b != 0 { + if b & 1 != 0 { + r ^= a; + } + let hi = a & 0x80; + a <<= 1; + if hi != 0 { + a ^= 0x1d; + } + b >>= 1; + } + r +} + +fn gpow(k: usize) -> u8 { + (0..k).fold(1, |a, _| gmul(a, 2)) +} + +/// Gera as imagens dos membros de um RAID 5/6 com a paridade na posição pedida. +fn encode_parity(level: Level, layout: Layout, n: usize, logical: &[u8]) -> Vec> { + let dd = if level == Level::R6 { n - 2 } else { n - 1 }; + let rows = logical.len() / (C * dd); + let mut disks = vec![vec![0u8; rows * C]; n]; + for row in 0..rows { + let r = row % n; + let (pd, qd) = match layout { + Layout::LeftSymmetric | Layout::LeftAsymmetric => { + let p = n - 1 - r; + (p, (p + 1) % n) + } + _ => (r, (r + 1) % n), + }; + let sym = matches!(layout, Layout::LeftSymmetric | Layout::RightSymmetric); + // ordem dos discos de dados + let order: Vec = if sym { + 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() + }; + let mut p = vec![0u8; C]; + let mut q = vec![0u8; C]; + for (k, &disk) in order.iter().enumerate() { + let src = &logical[(row * dd + k) * C..(row * dd + k + 1) * C]; + disks[disk][row * C..(row + 1) * C].copy_from_slice(src); + for i in 0..C { + p[i] ^= src[i]; + q[i] ^= gmul(gpow(k), src[i]); + } + } + disks[pd][row * C..(row + 1) * C].copy_from_slice(&p); + if level == Level::R6 { + disks[qd][row * C..(row + 1) * C].copy_from_slice(&q); + } + } + disks +} + +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 }) + }) + .collect() +} + +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(); + 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:?}"); + // leitura desalinhada + let part = read_vec(&r, 5000, 30000).unwrap(); + assert_eq!(part, &logical[5000..35000]); +} + +#[test] +fn raid5_all_layouts_intact_and_degraded() { + for layout in Layout::all_raid5() { + for n in [3, 4, 5] { + check(Level::R5, layout, n, &[]); + for m in 0..n { + check(Level::R5, layout, n, &[m]); + } + } + } +} + +#[test] +fn raid6_layouts_with_one_and_two_missing() { + for layout in Layout::all_raid5() { + for n in [4, 5, 6] { + check(Level::R6, layout, n, &[]); + for a in 0..n { + check(Level::R6, layout, n, &[a]); + for b in a + 1..n { + check(Level::R6, layout, n, &[a, b]); + } + } + } + } +} + +#[test] +fn raid0_raid1_linear_and_raid10_near() { + let logical = data(C * 24); + // RAID 0 com 3 discos + let mut imgs = vec![vec![]; 3]; + 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(); + 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(); + 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(); + assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical); + // RAID 10 near=2 com 4 discos: slots em sequência. + let n = 4; + let mut imgs = vec![vec![0u8; C * 12]; n]; + for (c, ch) in logical.chunks(C).enumerate() { + for j in 0..2 { + let slot = c * 2 + j; + let (disk, row) = (slot % n, slot / n); + 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])); + p.near = 2; + p.far = 1; + let r = RaidSource::new(p).unwrap(); + assert_eq!(r.size(), (C * 24) as u64); + 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 { + 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()); + b[16..32].fill(uuid); + b[32..36].copy_from_slice(b"test"); + b[72..76].copy_from_slice(&level.to_le_bytes()); + b[76..80].copy_from_slice(&layout.to_le_bytes()); + b[80..88].copy_from_slice(&data_sectors.to_le_bytes()); + b[88..92].copy_from_slice(&((C / 512) as u32).to_le_bytes()); + b[92..96].copy_from_slice(&raid_disks.to_le_bytes()); + b[128..136].copy_from_slice(&data_off_sectors.to_le_bytes()); + b[136..144].copy_from_slice(&data_sectors.to_le_bytes()); + b[160..164].copy_from_slice(&dev_number.to_le_bytes()); + b[200..208].copy_from_slice(&events.to_le_bytes()); + b[220..224].copy_from_slice(&raid_disks.to_le_bytes()); + for i in 0..raid_disks as usize { + b[256 + i * 2..258 + i * 2].copy_from_slice(&(i as u16).to_le_bytes()); + } + let _ = role; + b +} + +#[test] +fn assembles_from_md_v12_superblocks_with_a_missing_disk() { + let n = 4; + let logical = data(C * 3 * 40); + let imgs = encode_parity(Level::R5, Layout::LeftSymmetric, n, &logical); + let data_off = 2048u64; // 1 MiB em setores + let mut members_in = vec![]; + for (i, im) in imgs.iter().enumerate() { + if i == 2 { + 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); + 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 + let mut real = vec![0u8; data_off as usize * 512 + im.len()]; + real[4096..8192].copy_from_slice(&sb); + real[data_off as usize * 512..].copy_from_slice(im); + let src: Arc = Arc::new(MemSource(real)); + let sup = read_md_super(src.as_ref()).expect("superbloco"); + assert_eq!(sup.version, "1.2"); + assert_eq!(sup.role, Some(i as u32)); + members_in.push((src, sup)); + } + let arrays = assemble_md(members_in); + assert_eq!(arrays.len(), 1); + let a = arrays.into_iter().next().unwrap().unwrap(); + assert_eq!(a.missing, 1); + assert_eq!(a.info.level, Level::R5); + assert_eq!(a.source.size(), logical.len() as u64); + assert!(read_vec(a.source.as_ref(), 0, logical.len()).unwrap() == logical); +} diff --git a/scripts/fetch-testdata.sh b/scripts/fetch-testdata.sh index 3a069d6..8683307 100755 --- a/scripts/fetch-testdata.sh +++ b/scripts/fetch-testdata.sh @@ -12,4 +12,8 @@ for n in bitlk-images fvault2-images; do tar xJf "$tmp/$n.tar.xz" -C "$dest" rm -rf "$tmp" done +mkdir -p "$dest/dfvfs" +for f in lvm.raw apfs.raw apfs_encrypted.dmg hfsplus.raw hfs.raw xfs.raw xfs-bigtime.raw ext4.raw ext2.raw fvdetest.qcow2 luks1.raw luks2.raw bdetogo.raw; do + [ -f "$dest/dfvfs/$f" ] || curl -fsSL "https://raw.githubusercontent.com/log2timeline/dfvfs/main/test_data/$f" -o "$dest/dfvfs/$f" +done echo "ok: $dest"