Formatação (cargo fmt) e ajustes do clippy para o CI da 1.0.0
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
74218e7beb
commit
beb83fef92
49 files changed
+3806
-854
No files matched your search
@@ -240,14 +240,23 @@ impl AsmArgs {
|
|||||||
for k in &self.volume_keys {
|
for k in &self.volume_keys {
|
||||||
secrets.push(SecretInput::Key { hex: k.clone() });
|
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() {
|
let level = match self.raid_level.as_deref() {
|
||||||
None => None,
|
None => None,
|
||||||
Some("0") => Some(guara_core::raid::Level::R0),
|
Some("0") => Some(guara_core::raid::Level::R0),
|
||||||
Some("1") => Some(guara_core::raid::Level::R1),
|
Some("1") => Some(guara_core::raid::Level::R1),
|
||||||
Some("5") => Some(guara_core::raid::Level::R5),
|
Some("5") => Some(guara_core::raid::Level::R5),
|
||||||
Some("6") => Some(guara_core::raid::Level::R6),
|
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() {
|
let layout = match self.raid_layout.as_deref() {
|
||||||
None => None,
|
None => None,
|
||||||
@@ -344,7 +353,12 @@ fn run(cli: Cli) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
Cmd::Inspect { path, json, asm } => {
|
Cmd::Inspect { path, json, asm } => {
|
||||||
let a = asm.assembly()?;
|
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 {
|
if json {
|
||||||
println!("{}", serde_json::to_string_pretty(&v).unwrap());
|
println!("{}", serde_json::to_string_pretty(&v).unwrap());
|
||||||
} else {
|
} else {
|
||||||
@@ -434,7 +448,14 @@ fn run(cli: Cli) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
Cmd::Verify { path } => {
|
Cmd::Verify { path } => {
|
||||||
let e = guara_core::ewf::EwfSource::open(std::path::Path::new(&path)).map_err(s)?;
|
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);
|
println!("Segmentos: {}", e.info.segments);
|
||||||
for (k, v) in &e.info.fields {
|
for (k, v) in &e.info.fields {
|
||||||
println!("{k}: {v}");
|
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() {
|
let fmt = match format.as_str() {
|
||||||
"raw" => guara_core::image::ImageFormat::Raw,
|
"raw" => guara_core::image::ImageFormat::Raw,
|
||||||
"e01" => guara_core::image::ImageFormat::E01 {
|
"e01" => guara_core::image::ImageFormat::E01 {
|
||||||
@@ -464,8 +496,17 @@ fn run(cli: Cli) -> Result<(), String> {
|
|||||||
segment_mib,
|
segment_mib,
|
||||||
compression,
|
compression,
|
||||||
},
|
},
|
||||||
"aff4" => guara_core::image::ImageFormat::Aff4 { case_number: case, examiner, evidence_number: evidence, notes },
|
"aff4" => guara_core::image::ImageFormat::Aff4 {
|
||||||
x => return Err(format!("formato de imagem desconhecido: {x} (use raw, e01 ou 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 job = eng.start_image_as(&path, &dest, &fmt).map_err(s)?;
|
||||||
let st = wait(&eng, &job, None)?;
|
let st = wait(&eng, &job, None)?;
|
||||||
@@ -1007,4 +1048,3 @@ fn print_health(v: &Value) {
|
|||||||
println!("\n{}", adv.as_str().unwrap_or(""));
|
println!("\n{}", adv.as_str().unwrap_or(""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+170
-48
@@ -16,7 +16,10 @@ use std::sync::Mutex;
|
|||||||
|
|
||||||
pub fn is_aff4(head: &[u8]) -> bool {
|
pub fn is_aff4(head: &[u8]) -> bool {
|
||||||
// ZIP local file header + primeiro nome "container.description" ou "version.txt".
|
// 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 {
|
fn find(h: &[u8], n: &[u8]) -> bool {
|
||||||
@@ -35,7 +38,10 @@ struct Resource {
|
|||||||
|
|
||||||
impl Resource {
|
impl Resource {
|
||||||
fn get(&self, k: &str) -> Option<&str> {
|
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<u64> {
|
fn num(&self, k: &str) -> Option<u64> {
|
||||||
self.get(k)?.parse().ok()
|
self.get(k)?.parse().ok()
|
||||||
@@ -111,7 +117,10 @@ impl Turtle<'_> {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let s = self.i;
|
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;
|
self.i += 1;
|
||||||
}
|
}
|
||||||
// ponto final de sentença colado ao termo
|
// ponto final de sentença colado ao termo
|
||||||
@@ -128,7 +137,11 @@ impl Turtle<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_turtle(text: &str) -> HashMap<String, Resource> {
|
fn parse_turtle(text: &str) -> HashMap<String, Resource> {
|
||||||
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<String, Resource> = HashMap::new();
|
let mut out: HashMap<String, Resource> = HashMap::new();
|
||||||
loop {
|
loop {
|
||||||
t.ws();
|
t.ws();
|
||||||
@@ -153,12 +166,14 @@ fn parse_turtle(text: &str) -> HashMap<String, Resource> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// sujeito
|
// sujeito
|
||||||
let Some(subj) = (if t.b[t.i] == b':' && t.b.get(t.i + 1).map_or(true, |c| c.is_ascii_whitespace()) {
|
let Some(subj) =
|
||||||
t.i += 1;
|
(if t.b[t.i] == b':' && t.b.get(t.i + 1).map_or(true, |c| c.is_ascii_whitespace()) {
|
||||||
Some(t.prefixes.get("").cloned().unwrap_or_default())
|
t.i += 1;
|
||||||
} else {
|
Some(t.prefixes.get("").cloned().unwrap_or_default())
|
||||||
t.term()
|
} else {
|
||||||
}) else {
|
t.term()
|
||||||
|
})
|
||||||
|
else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
let res = out.entry(subj).or_default();
|
let res = out.entry(subj).or_default();
|
||||||
@@ -278,14 +293,23 @@ fn method_of(uri: &str) -> Method {
|
|||||||
impl Aff4Source {
|
impl Aff4Source {
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
let file = File::open(path)?;
|
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();
|
let mut entries = HashMap::new();
|
||||||
for i in 0..zip.len() {
|
for i in 0..zip.len() {
|
||||||
let f = zip.by_index_raw(i).map_err(|e| Error::msg(e.to_string()))?;
|
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 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();
|
let mut s = String::new();
|
||||||
f.read_to_string(&mut s)?;
|
f.read_to_string(&mut s)?;
|
||||||
s
|
s
|
||||||
@@ -297,19 +321,30 @@ impl Aff4Source {
|
|||||||
let image = res
|
let image = res
|
||||||
.values()
|
.values()
|
||||||
.find(|r| (r.is(&img_t) || r.is(&disk_t)) && r.get(&ds).is_some())
|
.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();
|
let mut info = HashMap::new();
|
||||||
if let Some(v) = image.get(&format!("{NS}diskModel")) {
|
if let Some(v) = image.get(&format!("{NS}diskModel")) {
|
||||||
info.insert("Modelo do disco".to_string(), v.to_string());
|
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}"))) {
|
if let Some(v) = res.values().find_map(|r| r.get(&format!("{NS}{k}"))) {
|
||||||
info.insert(label.to_string(), v.to_string());
|
info.insert(label.to_string(), v.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let data_urn = image.get(&ds).unwrap().to_string();
|
let data_urn = image.get(&ds).unwrap().to_string();
|
||||||
// Hashes gravados no fluxo de dados (ou no mapa).
|
// 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() {
|
for h in r.props.get(&format!("{NS}hash")).into_iter().flatten() {
|
||||||
match h.len() {
|
match h.len() {
|
||||||
32 if h.chars().all(|c| c.is_ascii_hexdigit()) => {
|
32 if h.chars().all(|c| c.is_ascii_hexdigit()) => {
|
||||||
@@ -333,7 +368,9 @@ impl Aff4Source {
|
|||||||
cache: Mutex::new(None),
|
cache: Mutex::new(None),
|
||||||
index_cache: Mutex::new(HashMap::new()),
|
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");
|
let map_t = format!("{NS}Map");
|
||||||
if top.is(&map_t) {
|
if top.is(&map_t) {
|
||||||
me.size = top.num(&format!("{NS}size")).unwrap_or(0);
|
me.size = top.num(&format!("{NS}size")).unwrap_or(0);
|
||||||
@@ -345,7 +382,8 @@ impl Aff4Source {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(sym) = line.strip_prefix(&format!("{NS}SymbolicStream")) {
|
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));
|
me.targets.push(Target::Fill(b));
|
||||||
} else if line == format!("{NS}Zero") {
|
} else if line == format!("{NS}Zero") {
|
||||||
me.targets.push(Target::Zero);
|
me.targets.push(Target::Zero);
|
||||||
@@ -372,7 +410,12 @@ impl Aff4Source {
|
|||||||
me.size = s.size;
|
me.size = s.size;
|
||||||
me.streams.push(s);
|
me.streams.push(s);
|
||||||
me.targets.push(Target::Stream(0));
|
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 {
|
if me.size == 0 {
|
||||||
me.size = image.num(&format!("{NS}size")).unwrap_or(0);
|
me.size = image.num(&format!("{NS}size")).unwrap_or(0);
|
||||||
@@ -381,20 +424,29 @@ impl Aff4Source {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_stream(&self, res: &HashMap<String, Resource>, urn: &str) -> Result<ImageStream> {
|
fn load_stream(&self, res: &HashMap<String, Resource>, urn: &str) -> Result<ImageStream> {
|
||||||
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 {
|
Ok(ImageStream {
|
||||||
urn: urn.to_string(),
|
urn: urn.to_string(),
|
||||||
chunk: r.num(&format!("{NS}chunkSize")).unwrap_or(32768),
|
chunk: r.num(&format!("{NS}chunkSize")).unwrap_or(32768),
|
||||||
per_bevy: r.num(&format!("{NS}chunksInSegment")).unwrap_or(2048),
|
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),
|
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.
|
/// 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<Option<bool>> {
|
pub fn verify_stream_md5(&self) -> Result<Option<bool>> {
|
||||||
let Some(want) = self.info.get("md5").cloned() else { return Ok(None) };
|
let Some(want) = self.info.get("md5").cloned() else {
|
||||||
let Some(st) = self.streams.first() else { return Ok(None) };
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(st) = self.streams.first() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
let mut h = MultiHasher::new();
|
let mut h = MultiHasher::new();
|
||||||
let mut buf = vec![0u8; st.chunk as usize];
|
let mut buf = vec![0u8; st.chunk as usize];
|
||||||
let mut pos = 0u64;
|
let mut pos = 0u64;
|
||||||
@@ -408,7 +460,10 @@ impl Aff4Source {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_entry(&self, name: &str) -> Result<Vec<u8>> {
|
fn read_entry(&self, name: &str) -> Result<Vec<u8>> {
|
||||||
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];
|
let mut v = vec![0u8; loc.size as usize];
|
||||||
read_exact_at_file(&self.file, loc.data_start, &mut v)?;
|
read_exact_at_file(&self.file, loc.data_start, &mut v)?;
|
||||||
Ok(v)
|
Ok(v)
|
||||||
@@ -431,7 +486,9 @@ impl Aff4Source {
|
|||||||
if let Some(i) = ic.get(&(si, bevy)) {
|
if let Some(i) = ic.get(&(si, bevy)) {
|
||||||
i.clone()
|
i.clone()
|
||||||
} else {
|
} 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];
|
let mut v = vec![0u8; loc.size as usize];
|
||||||
read_exact_at_file(&self.file, loc.data_start, &mut v)?;
|
read_exact_at_file(&self.file, loc.data_start, &mut v)?;
|
||||||
let a = std::sync::Arc::new(v);
|
let a = std::sync::Arc::new(v);
|
||||||
@@ -439,9 +496,17 @@ impl Aff4Source {
|
|||||||
a
|
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 e = index
|
||||||
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);
|
.get(within as usize * 12..within as usize * 12 + 12)
|
||||||
let loc = self.entries.get(&base).ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "bevy ausente"))?;
|
.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];
|
let mut raw = vec![0u8; len];
|
||||||
read_exact_at_file(&self.file, loc.data_start + off, &mut raw)?;
|
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;
|
let want = st.chunk.min(st.size.saturating_sub(chunk_no * st.chunk)) as usize;
|
||||||
@@ -450,16 +515,23 @@ impl Aff4Source {
|
|||||||
} else {
|
} else {
|
||||||
let bad = |m: String| io::Error::new(io::ErrorKind::InvalidData, m);
|
let bad = |m: String| io::Error::new(io::ErrorKind::InvalidData, m);
|
||||||
match st.method {
|
match st.method {
|
||||||
Method::Snappy => snap::raw::Decoder::new().decompress_vec(&raw).map_err(|e| bad(format!("snappy: {e}")))?,
|
Method::Snappy => snap::raw::Decoder::new()
|
||||||
Method::Lz4 => lz4_flex::block::decompress(&raw, st.chunk as usize).map_err(|e| bad(format!("lz4: {e}")))?,
|
.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 => {
|
Method::Deflate => {
|
||||||
let mut v = Vec::with_capacity(st.chunk as usize);
|
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
|
v
|
||||||
}
|
}
|
||||||
Method::Zlib => {
|
Method::Zlib => {
|
||||||
let mut v = Vec::with_capacity(st.chunk as usize);
|
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
|
v
|
||||||
}
|
}
|
||||||
Method::Stored => unreachable!(),
|
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;
|
let mut done = 0;
|
||||||
while done < buf.len() {
|
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 {
|
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;
|
done += n;
|
||||||
}
|
}
|
||||||
@@ -534,7 +610,9 @@ impl Source for Aff4Source {
|
|||||||
let n = ((e.start + e.len - pos) as usize).min(want - done);
|
let n = ((e.start + e.len - pos) as usize).min(want - done);
|
||||||
let toff = e.target_off + (pos - e.start);
|
let toff = e.target_off + (pos - e.start);
|
||||||
match self.targets.get(e.target as usize) {
|
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),
|
Some(Target::Fill(b)) => buf[done..done + n].fill(*b),
|
||||||
_ => buf[done..done + n].fill(0),
|
_ => buf[done..done + n].fill(0),
|
||||||
}
|
}
|
||||||
@@ -557,7 +635,13 @@ pub struct Aff4Options {
|
|||||||
|
|
||||||
impl Default for Aff4Options {
|
impl Default for Aff4Options {
|
||||||
fn default() -> Self {
|
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 mut b = [0u8; 16];
|
||||||
let _ = std::fs::File::open("/dev/urandom").and_then(|mut f| f.read_exact(&mut b));
|
let _ = std::fs::File::open("/dev/urandom").and_then(|mut f| f.read_exact(&mut b));
|
||||||
if b == [0u8; 16] {
|
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 = (t ^ (std::process::id() as u128) << 64).to_le_bytes();
|
||||||
}
|
}
|
||||||
b[6] = (b[6] & 0x0F) | 0x40;
|
b[6] = (b[6] & 0x0F) | 0x40;
|
||||||
b[8] = (b[8] & 0x3F) | 0x80;
|
b[8] = (b[8] & 0x3F) | 0x80;
|
||||||
let h: Vec<String> = b.iter().map(|x| format!("{x:02x}")).collect();
|
let h: Vec<String> = 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 {
|
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 days = secs.div_euclid(86400);
|
||||||
let rem = secs.rem_euclid(86400);
|
let rem = secs.rem_euclid(86400);
|
||||||
let z = days + 719468;
|
let z = days + 719468;
|
||||||
@@ -588,11 +685,18 @@ fn iso_now() -> String {
|
|||||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
let y = if m <= 2 { y + 1 } else { y };
|
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 {
|
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`.
|
/// Grava uma imagem AFF4 linear (um fluxo de blocos snappy) de `src`.
|
||||||
@@ -607,7 +711,12 @@ pub fn write_aff4(
|
|||||||
.write(true)
|
.write(true)
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.open(dest)
|
.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);
|
crate::owner::give_to_user(dest);
|
||||||
let total = src.size();
|
let total = src.size();
|
||||||
let chunk = opts.chunk_size as u64;
|
let chunk = opts.chunk_size as u64;
|
||||||
@@ -616,9 +725,12 @@ pub fn write_aff4(
|
|||||||
let stream = format!("aff4://{}", new_uuid());
|
let stream = format!("aff4://{}", new_uuid());
|
||||||
let image = format!("aff4://{}", new_uuid());
|
let image = format!("aff4://{}", new_uuid());
|
||||||
let mut zip = zip::ZipWriter::new(out);
|
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<ImageReport> {
|
let result = (|| -> Result<ImageReport> {
|
||||||
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.write_all(volume.as_bytes())?;
|
||||||
zip.start_file("version.txt", stored).map_err(zerr)?;
|
zip.start_file("version.txt", stored).map_err(zerr)?;
|
||||||
zip.write_all(format!("major=1\nminor=2\ntool=Guara {}\n", crate::VERSION).as_bytes())?;
|
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);
|
hasher.update(b);
|
||||||
// bloco curto (último) é guardado como está; comprimido só se ficar menor
|
// 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).
|
// 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)?;
|
zip.write_all(data)?;
|
||||||
index.extend_from_slice(&boff.to_le_bytes());
|
index.extend_from_slice(&boff.to_le_bytes());
|
||||||
index.extend_from_slice(&(data.len() as u32).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;
|
chunk_no += 1;
|
||||||
progress(pos, total);
|
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)?;
|
zip.write_all(&index)?;
|
||||||
if rep.cancelled {
|
if rep.cancelled {
|
||||||
break;
|
break;
|
||||||
@@ -708,7 +827,10 @@ pub fn write_aff4(
|
|||||||
match result {
|
match result {
|
||||||
Ok(rep) => {
|
Ok(rep) => {
|
||||||
zip.finish().map_err(zerr)?;
|
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)
|
Ok(rep)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -15,12 +15,20 @@ use std::sync::Arc;
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||||
pub enum SecretInput {
|
pub enum SecretInput {
|
||||||
Password { value: String },
|
Password {
|
||||||
Recovery { value: String },
|
value: String,
|
||||||
|
},
|
||||||
|
Recovery {
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
/// Caminho de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS).
|
/// Caminho de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS).
|
||||||
Keyfile { path: String },
|
Keyfile {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
/// Chave do volume em hexadecimal.
|
/// 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).
|
/// Chave de volume guardada na sessão (só quando o usuário pede para lembrar).
|
||||||
@@ -62,7 +70,10 @@ pub struct Assembly {
|
|||||||
|
|
||||||
impl Assembly {
|
impl Assembly {
|
||||||
pub fn is_plain(&self) -> bool {
|
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<Vec<Secret>> {
|
|||||||
v.push(match s {
|
v.push(match s {
|
||||||
SecretInput::Password { value } => Secret::Password(value.clone()),
|
SecretInput::Password { value } => Secret::Password(value.clone()),
|
||||||
SecretInput::Recovery { value } => Secret::RecoveryKey(value.clone()),
|
SecretInput::Recovery { value } => Secret::RecoveryKey(value.clone()),
|
||||||
SecretInput::Keyfile { path } => Secret::KeyFile(
|
SecretInput::Keyfile { path } => Secret::KeyFile(std::fs::read(path).map_err(|e| {
|
||||||
std::fs::read(path).map_err(|e| Error::msg(format!("não foi possível ler o arquivo de chave {path}: {e}")))?,
|
Error::msg(format!(
|
||||||
),
|
"não foi possível ler o arquivo de chave {path}: {e}"
|
||||||
|
))
|
||||||
|
})?),
|
||||||
SecretInput::Key { hex } => Secret::VolumeKey(
|
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.
|
/// Coloca `src` em `start`; se cair dentro de uma região virtual, a substitui por dentro.
|
||||||
fn patch(regions: &mut Vec<Region>, start: u64, src: Arc<dyn Source>, label: String) {
|
fn patch(regions: &mut Vec<Region>, start: u64, src: Arc<dyn Source>, label: String) {
|
||||||
let len = src.size();
|
let len = src.size();
|
||||||
if let Some(r) = regions.iter_mut().find(|r| r.start <= start && start < r.start + r.len) {
|
if let Some(r) = regions
|
||||||
let inner = Region { start: start - r.start, len, src, label: label.clone() };
|
.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]);
|
let wrapped = Overlay::new(r.src.clone(), vec![inner]);
|
||||||
if start == r.start {
|
if start == r.start {
|
||||||
r.len = r.len.max(len);
|
r.len = r.len.max(len);
|
||||||
@@ -151,7 +173,12 @@ fn patch(regions: &mut Vec<Region>, start: u64, src: Arc<dyn Source>, label: Str
|
|||||||
r.src = Arc::new(wrapped);
|
r.src = Arc::new(wrapped);
|
||||||
return;
|
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).
|
/// 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<dyn Source>, primary: bool) -> Vec<Window> {
|
fn windows_of(src: &Arc<dyn Source>, primary: bool) -> Vec<Window> {
|
||||||
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);
|
let layout = partition::analyze(src.as_ref(), false);
|
||||||
for p in &layout.partitions {
|
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 {
|
v.push(Window {
|
||||||
src: Arc::new(SubSource::new(src.clone(), p.start, len)),
|
src: Arc::new(SubSource::new(src.clone(), p.start, len)),
|
||||||
primary_start: primary.then_some(p.start),
|
primary_start: primary.then_some(p.start),
|
||||||
@@ -196,11 +230,21 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
let mut disks: Vec<Arc<dyn Source>> = vec![base.clone()];
|
let mut disks: Vec<Arc<dyn Source>> = vec![base.clone()];
|
||||||
disks.extend(members.iter().cloned());
|
disks.extend(members.iter().cloned());
|
||||||
let params = pick_raid(&disks, spec, &mut out.notes)?;
|
let params = pick_raid(&disks, spec, &mut out.notes)?;
|
||||||
let src: Arc<dyn Source> = Arc::new(crate::raid::RaidSource::new(params).map_err(Error::msg)?);
|
let src: Arc<dyn Source> =
|
||||||
out.notes.push(format!("RAID montado a partir de {} disco(s) sem metadados.", disks.len()));
|
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;
|
out.skip_primary = true;
|
||||||
members.clear();
|
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<String> = Default::default();
|
let mut done_containers: std::collections::BTreeSet<String> = Default::default();
|
||||||
let mut tried_pool = false;
|
let mut tried_pool = false;
|
||||||
@@ -219,11 +263,21 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
// 1) RAID e LVM: reúne janelas do disco principal e dos membros extras.
|
// 1) RAID e LVM: reúne janelas do disco principal e dos membros extras.
|
||||||
if round == 0 || !tried_pool || !out.extra_parts.is_empty() {
|
if round == 0 || !tried_pool || !out.extra_parts.is_empty() {
|
||||||
let mut pool: Vec<Window> = vec![];
|
let mut pool: Vec<Window> = 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 {
|
for p in &all_parts {
|
||||||
if matches!(p.fs, FsKind::Raid | FsKind::Lvm) {
|
if matches!(p.fs, FsKind::Raid | FsKind::Lvm) {
|
||||||
let len = if p.len == 0 { current.size().saturating_sub(p.start) } else { p.len };
|
let len = if p.len == 0 {
|
||||||
pool.push(Window { src: Arc::new(SubSource::new(current.clone(), p.start, len)), primary_start: Some(p.start) });
|
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 {
|
for m in &members {
|
||||||
@@ -241,7 +295,12 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
md.push((w.src.clone(), sup));
|
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();
|
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() {
|
if !new_md.is_empty() {
|
||||||
for (_, s) in &new_md {
|
for (_, s) in &new_md {
|
||||||
done_containers.insert(format!("md{}", crypt::to_hex(&s.uuid)));
|
done_containers.insert(format!("md{}", crypt::to_hex(&s.uuid)));
|
||||||
@@ -249,13 +308,30 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
for res in crate::raid::assemble_md(new_md) {
|
for res in crate::raid::assemble_md(new_md) {
|
||||||
match res {
|
match res {
|
||||||
Ok(a) => {
|
Ok(a) => {
|
||||||
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 {
|
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 {
|
} else {
|
||||||
out.notes.push(format!("{label} montado."));
|
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;
|
progress = true;
|
||||||
}
|
}
|
||||||
Err(e) => out.notes.push(format!("RAID não montado: {e}")),
|
Err(e) => out.notes.push(format!("RAID não montado: {e}")),
|
||||||
@@ -267,7 +343,10 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
let mut pvs: Vec<(Arc<dyn Source>, crate::lvm::PvInfo)> = vec![];
|
let mut pvs: Vec<(Arc<dyn Source>, crate::lvm::PvInfo)> = vec![];
|
||||||
let mut pv_primary: Vec<u64> = vec![];
|
let mut pv_primary: Vec<u64> = vec![];
|
||||||
let md_srcs: Vec<Arc<dyn Source>> = regions.iter().map(|r| r.src.clone()).collect();
|
let md_srcs: Vec<Arc<dyn Source>> = regions.iter().map(|r| r.src.clone()).collect();
|
||||||
let mut lvm_pool: Vec<(Arc<dyn Source>, Option<u64>)> = pool.iter().map(|w| (w.src.clone(), w.primary_start)).collect();
|
let mut lvm_pool: Vec<(Arc<dyn Source>, Option<u64>)> = pool
|
||||||
|
.iter()
|
||||||
|
.map(|w| (w.src.clone(), w.primary_start))
|
||||||
|
.collect();
|
||||||
for (r, s) in regions.iter().zip(md_srcs) {
|
for (r, s) in regions.iter().zip(md_srcs) {
|
||||||
lvm_pool.push((s, Some(r.start)));
|
lvm_pool.push((s, Some(r.start)));
|
||||||
}
|
}
|
||||||
@@ -288,7 +367,13 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
out.notes.push(format!("LVM {}: {w}", vg.name));
|
out.notes.push(format!("LVM {}: {w}", vg.name));
|
||||||
}
|
}
|
||||||
for (name, lv) in vg.lvs {
|
for (name, lv) in vg.lvs {
|
||||||
add_virtual(&mut regions, &mut next_virtual, &mut out, lv, format!("LVM {}/{}", vg.name, name));
|
add_virtual(
|
||||||
|
&mut regions,
|
||||||
|
&mut next_virtual,
|
||||||
|
&mut out,
|
||||||
|
lv,
|
||||||
|
format!("LVM {}/{}", vg.name, name),
|
||||||
|
);
|
||||||
progress = true;
|
progress = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,7 +395,11 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
if p.encrypted.is_none() || out.unlocked.iter().any(|u| u.start == p.start) {
|
if p.encrypted.is_none() || out.unlocked.iter().any(|u| u.start == p.start) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let len = if p.len == 0 { current.size().saturating_sub(p.start) } else { p.len };
|
let len = if p.len == 0 {
|
||||||
|
current.size().saturating_sub(p.start)
|
||||||
|
} else {
|
||||||
|
p.len
|
||||||
|
};
|
||||||
let window: Arc<dyn Source> = Arc::new(SubSource::new(current.clone(), p.start, len));
|
let window: Arc<dyn Source> = Arc::new(SubSource::new(current.clone(), p.start, len));
|
||||||
let mut all = secrets.clone();
|
let mut all = secrets.clone();
|
||||||
for k in asm.keys.iter().filter(|k| k.start == p.start) {
|
for k in asm.keys.iter().filter(|k| k.start == p.start) {
|
||||||
@@ -366,7 +455,13 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Anexa um volume virtual depois do fim do disco e registra as partições que ele contém.
|
/// Anexa um volume virtual depois do fim do disco e registra as partições que ele contém.
|
||||||
fn add_virtual(regions: &mut Vec<Region>, next: &mut u64, out: &mut Assembled, src: Arc<dyn Source>, label: String) {
|
fn add_virtual(
|
||||||
|
regions: &mut Vec<Region>,
|
||||||
|
next: &mut u64,
|
||||||
|
out: &mut Assembled,
|
||||||
|
src: Arc<dyn Source>,
|
||||||
|
label: String,
|
||||||
|
) {
|
||||||
let start = *next;
|
let start = *next;
|
||||||
let len = src.size();
|
let len = src.size();
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
@@ -393,15 +488,34 @@ fn add_virtual(regions: &mut Vec<Region>, next: &mut u64, out: &mut Assembled, s
|
|||||||
out.extra_parts.push(q);
|
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.
|
/// Escolhe os parâmetros do RAID: o que o usuário deu manda; o resto vem da detecção.
|
||||||
fn pick_raid(disks: &[Arc<dyn Source>], spec: &RaidSpec, notes: &mut Vec<String>) -> Result<crate::raid::RaidParams> {
|
fn pick_raid(
|
||||||
|
disks: &[Arc<dyn Source>],
|
||||||
|
spec: &RaidSpec,
|
||||||
|
notes: &mut Vec<String>,
|
||||||
|
) -> Result<crate::raid::RaidParams> {
|
||||||
use crate::raid::{Member, RaidParams};
|
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()) {
|
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));
|
return Ok(RaidParams::simple(level, chunk * 1024, layout, members));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,12 @@ fn le64(b: &[u8], o: usize) -> u64 {
|
|||||||
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
|
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_NORMAL: [u8; 16] = [
|
||||||
const GUID_EOW: [u8; 16] = [0x3b, 0x4d, 0xa8, 0x92, 0x80, 0xdd, 0x0e, 0x4d, 0x9e, 0x4e, 0xb1, 0xe3, 0x28, 0x4e, 0xae, 0xd8];
|
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).
|
/// Posição da tabela de deslocamentos FVE, se for BitLocker (normal ou To Go).
|
||||||
fn offsets_pos(vbr: &[u8]) -> Option<usize> {
|
fn offsets_pos(vbr: &[u8]) -> Option<usize> {
|
||||||
@@ -45,7 +49,9 @@ const RA: [u32; 4] = [9, 0, 13, 0];
|
|||||||
const RB: [u32; 4] = [0, 10, 0, 25];
|
const RB: [u32; 4] = [0, 10, 0, 25];
|
||||||
|
|
||||||
fn words(buf: &[u8]) -> Vec<u32> {
|
fn words(buf: &[u8]) -> Vec<u32> {
|
||||||
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.
|
// 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)
|
self.nested.iter().find(|e| e.vtype == 5)
|
||||||
}
|
}
|
||||||
fn stretch_salt(&self) -> Option<&[u8]> {
|
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<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let fve = fve.ok_or_else(|| UnlockError::Unsupported("metadados FVE ilegíveis ou do Windows Vista".into()))?;
|
let fve = fve.ok_or_else(|| {
|
||||||
let fvek_entry = fve.fvek.as_ref().ok_or_else(|| UnlockError::Corrupt("sem FVEK".into()))?;
|
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<Vec<u8>> = None;
|
let mut volume_key: Option<Vec<u8>> = None;
|
||||||
for s in secrets {
|
for s in secrets {
|
||||||
@@ -393,7 +408,8 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
k
|
k
|
||||||
} else {
|
} else {
|
||||||
let vmk = vmk_found.ok_or(UnlockError::WrongSecret)?;
|
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 k = &fvek_bytes;
|
||||||
let bad = || UnlockError::Corrupt("tamanho de chave incompatível com o método".into());
|
let bad = || UnlockError::Corrupt("tamanho de chave incompatível com o método".into());
|
||||||
@@ -408,7 +424,11 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
key: AesAny::new(&k[..n]).ok_or_else(bad)?,
|
key: AesAny::new(&k[..n]).ok_or_else(bad)?,
|
||||||
tweak: AesAny::new(&k[32..32 + 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 => {
|
0x8002 | 0x8003 => {
|
||||||
@@ -417,35 +437,69 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
return Err(bad());
|
return Err(bad());
|
||||||
}
|
}
|
||||||
let key = AesAny::new(&k[..n]).ok_or_else(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 => {
|
0x8004 | 0x8005 => {
|
||||||
let n = if fve.method == 0x8004 { 16 } else { 32 };
|
let n = if fve.method == 0x8004 { 16 } else { 32 };
|
||||||
if k.len() < 2 * n {
|
if k.len() < 2 * n {
|
||||||
return Err(bad());
|
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 {
|
SectorCipher::Xts {
|
||||||
k1: AesAny::new(&k[..n]).ok_or_else(bad)?,
|
k1: AesAny::new(&k[..n]).ok_or_else(bad)?,
|
||||||
k2: AesAny::new(&k[n..2 * n]).ok_or_else(bad)?,
|
k2: AesAny::new(&k[n..2 * n]).ok_or_else(bad)?,
|
||||||
iv,
|
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 len = src.size();
|
||||||
let mut cs = CryptSource::new(src.clone(), cipher, 0, len, bps);
|
let mut cs = CryptSource::new(src.clone(), cipher, 0, len, bps);
|
||||||
let mut patches = vec![];
|
let mut patches = vec![];
|
||||||
// Áreas de metadados FVE: o Windows não as expõe como dados.
|
// Áreas de metadados FVE: o Windows não as expõe como dados.
|
||||||
for off in meta_offsets {
|
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 {
|
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á.
|
// 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 {
|
patches.push(Patch {
|
||||||
at: 0,
|
at: 0,
|
||||||
len: fve.volume_header_size,
|
len: fve.volume_header_size,
|
||||||
@@ -465,6 +519,11 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unlock_window(base: Arc<dyn Source>, start: u64, len: u64, secrets: &[Secret]) -> UnlockResult {
|
pub fn unlock_window(
|
||||||
|
base: Arc<dyn Source>,
|
||||||
|
start: u64,
|
||||||
|
len: u64,
|
||||||
|
secrets: &[Secret],
|
||||||
|
) -> UnlockResult {
|
||||||
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
||||||
}
|
}
|
||||||
@@ -80,12 +80,18 @@ fn plist_value(xml: &str, key: &str) -> Option<String> {
|
|||||||
while let Some(p) = xml[from..].find(&k) {
|
while let Some(p) = xml[from..].find(&k) {
|
||||||
let rest = &xml[from + p + k.len()..];
|
let rest = &xml[from + p + k.len()..];
|
||||||
from += p + k.len();
|
from += p + k.len();
|
||||||
let Some(open) = rest.find('<').filter(|&o| o == 0) else { continue };
|
let Some(open) = rest.find('<').filter(|&o| o == 0) else {
|
||||||
let Some(gt) = rest[open..].find('>') else { continue };
|
continue;
|
||||||
|
};
|
||||||
|
let Some(gt) = rest[open..].find('>') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if rest[open..open + gt].ends_with('/') {
|
if rest[open..open + gt].ends_with('/') {
|
||||||
continue; // <reference .../>
|
continue; // <reference .../>
|
||||||
}
|
}
|
||||||
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 tag = &rest[open + 1..open + 1 + te];
|
||||||
let vstart = open + gt + 1;
|
let vstart = open + gt + 1;
|
||||||
if let Some(e) = rest[vstart..].find(&format!("</{tag}>")) {
|
if let Some(e) = rest[vstart..].find(&format!("</{tag}>")) {
|
||||||
@@ -116,20 +122,31 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
let block_size = le32(&h, 96) as u64;
|
let block_size = le32(&h, 96) as u64;
|
||||||
let disklbl = le64(&h, 104);
|
let disklbl = le64(&h, 104);
|
||||||
if block_size == 0 || block_size > 1 << 20 || le32(&h, 168) != 16 {
|
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];
|
let mut md_key = [0u8; 32];
|
||||||
md_key[..16].copy_from_slice(&h[176..192]);
|
md_key[..16].copy_from_slice(&h[176..192]);
|
||||||
md_key[16..].copy_from_slice(&h[304..320]);
|
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 (k1, k2) = (
|
||||||
let md_cipher = SectorCipher::Xts { k1, k2, iv: Iv::Plain64 };
|
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 {
|
if label.len() < MD_BLOCK {
|
||||||
return Err(corrupt("rótulo do disco truncado"));
|
return Err(corrupt("rótulo do disco truncado"));
|
||||||
}
|
}
|
||||||
let vgd_off = le32(&label, 220) as u64;
|
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 {
|
if vgd.len() < 40 {
|
||||||
return Err(corrupt("descritor de grupos truncado"));
|
return Err(corrupt("descritor de grupos truncado"));
|
||||||
}
|
}
|
||||||
@@ -141,7 +158,8 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
let mut family = [0u8; 16];
|
let mut family = [0u8; 16];
|
||||||
let mut got = 0u8;
|
let mut got = 0u8;
|
||||||
for i in 0..md_blocks.min(64) {
|
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) {
|
if b.len() < MD_BLOCK || b.iter().all(|&x| x == 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -192,7 +210,12 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
let finish = |vk16: [u8; 16]| -> UnlockResult {
|
let finish = |vk16: [u8; 16]| -> UnlockResult {
|
||||||
let mut full = [0u8; 32];
|
let mut full = [0u8; 32];
|
||||||
full[..16].copy_from_slice(&vk16);
|
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 {
|
let cipher = SectorCipher::Xts {
|
||||||
k1: AesAny::new(&full[..16]).unwrap(),
|
k1: AesAny::new(&full[..16]).unwrap(),
|
||||||
k2: AesAny::new(&full[16..]).unwrap(),
|
k2: AesAny::new(&full[16..]).unwrap(),
|
||||||
@@ -228,6 +251,11 @@ pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
Err(UnlockError::WrongSecret)
|
Err(UnlockError::WrongSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unlock_window(base: Arc<dyn Source>, start: u64, len: u64, secrets: &[Secret]) -> UnlockResult {
|
pub fn unlock_window(
|
||||||
|
base: Arc<dyn Source>,
|
||||||
|
start: u64,
|
||||||
|
len: u64,
|
||||||
|
secrets: &[Secret],
|
||||||
|
) -> UnlockResult {
|
||||||
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
||||||
}
|
}
|
||||||
@@ -89,9 +89,13 @@ pub fn af_merge(src: &[u8], block: usize, stripes: usize, h: Hash) -> Vec<u8> {
|
|||||||
|
|
||||||
/// Cifra descrita por texto como "aes-xts-plain64" ou "aes-cbc-essiv:sha256".
|
/// Cifra descrita por texto como "aes-xts-plain64" ou "aes-cbc-essiv:sha256".
|
||||||
pub fn parse_cipher(spec: &str, key: &[u8]) -> std::result::Result<SectorCipher, UnlockError> {
|
pub fn parse_cipher(spec: &str, key: &[u8]) -> std::result::Result<SectorCipher, UnlockError> {
|
||||||
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") {
|
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 (mode, ivs) = rest.split_once('-').unwrap_or((rest, "plain"));
|
||||||
let bad = || UnlockError::Unsupported(format!("chave inválida para {spec}"));
|
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<SectorCipher,
|
|||||||
"plain" | "plain32" => Iv::Plain32,
|
"plain" | "plain32" => Iv::Plain32,
|
||||||
"plain64be" => Iv::Plain64Be,
|
"plain64be" => Iv::Plain64Be,
|
||||||
s if s.starts_with("essiv:") => {
|
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)?)
|
Iv::Essiv(AesAny::new(&h.digest(&[k])).ok_or_else(bad)?)
|
||||||
}
|
}
|
||||||
_ => return Err(UnlockError::Unsupported(format!("IV {ivs}"))),
|
_ => return Err(UnlockError::Unsupported(format!("IV {ivs}"))),
|
||||||
@@ -181,7 +186,8 @@ fn unlock_v1(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
}
|
}
|
||||||
let cipher_name = cstr(&h[8..40]);
|
let cipher_name = cstr(&h[8..40]);
|
||||||
let cipher_mode = cstr(&h[40..72]);
|
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 payload = be32(&h, 104) as u64 * 512;
|
||||||
let key_bytes = be32(&h, 108) as usize;
|
let key_bytes = be32(&h, 108) as usize;
|
||||||
let mk_digest = &h[112..132];
|
let mk_digest = &h[112..132];
|
||||||
@@ -221,7 +227,9 @@ fn unlock_v1(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let km_len = (key_bytes * stripes).div_ceil(512) * 512;
|
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 {
|
if km.len() < km_len {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -284,25 +292,38 @@ fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
}
|
}
|
||||||
let hdr_size = u64::from_be_bytes(h[8..16].try_into().unwrap());
|
let hdr_size = u64::from_be_bytes(h[8..16].try_into().unwrap());
|
||||||
if !(16384..=(4 << 20)).contains(&hdr_size) {
|
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 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 json: serde_json::Value = serde_json::from_slice(&area[..end])
|
||||||
let segments = json.get("segments").and_then(|s| s.as_object()).ok_or_else(|| UnlockError::Corrupt("sem segments".into()))?;
|
.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
|
let (_, seg) = segments
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, s)| jstr(s, "type") == Some("crypt"))
|
.find(|(_, s)| jstr(s, "type") == Some("crypt"))
|
||||||
.ok_or_else(|| UnlockError::Unsupported("sem segmento cifrado".into()))?;
|
.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()) {
|
let seg_len = match seg.get("size").and_then(|s| s.as_str()) {
|
||||||
Some("dynamic") | None => src.size().saturating_sub(seg_off),
|
Some("dynamic") | None => src.size().saturating_sub(seg_off),
|
||||||
Some(n) => n.parse().unwrap_or(0),
|
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 sector = jnum(seg, "sector_size").unwrap_or(512);
|
||||||
let iv_tweak = jnum(seg, "iv_tweak").unwrap_or(0);
|
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 {
|
let check = |vk: &[u8]| -> bool {
|
||||||
for (_, d) in digests {
|
for (_, d) in digests {
|
||||||
@@ -325,7 +346,8 @@ fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
};
|
};
|
||||||
let finish = |vk: Vec<u8>| -> UnlockResult {
|
let finish = |vk: Vec<u8>| -> UnlockResult {
|
||||||
let cipher = parse_cipher(&seg_enc, &vk)?;
|
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 {
|
Ok(Unlocked {
|
||||||
source: Arc::new(cs),
|
source: Arc::new(cs),
|
||||||
description: format!("LUKS2 ({seg_enc})"),
|
description: format!("LUKS2 ({seg_enc})"),
|
||||||
@@ -338,7 +360,10 @@ fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let pws = passwords(secrets);
|
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<String> = None;
|
let mut unsupported: Option<String> = None;
|
||||||
for (_, slot) in slots {
|
for (_, slot) in slots {
|
||||||
if jstr(slot, "type") != Some("luks2") {
|
if jstr(slot, "type") != Some("luks2") {
|
||||||
@@ -347,10 +372,20 @@ fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
let key_size = jnum(slot, "key_size").unwrap_or(0) as usize;
|
let key_size = jnum(slot, "key_size").unwrap_or(0) as usize;
|
||||||
let af = slot.get("af");
|
let af = slot.get("af");
|
||||||
let stripes = af.and_then(|a| jnum(a, "stripes")).unwrap_or(4000) as usize;
|
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 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 (Some(a_off), Some(a_size)) = (
|
||||||
let a_enc = ar.and_then(|a| jstr(a, "encryption")).unwrap_or("aes-xts-plain64");
|
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 a_ksize = ar.and_then(|a| jnum(a, "key_size")).unwrap_or(64) as usize;
|
||||||
let kdf = slot.get("kdf");
|
let kdf = slot.get("kdf");
|
||||||
let Some(kdf) = kdf else { continue };
|
let Some(kdf) = kdf else { continue };
|
||||||
@@ -359,18 +394,31 @@ fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
if key_size == 0 || stripes == 0 || km_len as u64 > a_size {
|
if key_size == 0 || stripes == 0 || km_len as u64 > a_size {
|
||||||
continue;
|
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 {
|
if km.len() < km_len {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for pw in &pws {
|
for pw in &pws {
|
||||||
let dk: Vec<u8> = match jstr(kdf, "type") {
|
let dk: Vec<u8> = match jstr(kdf, "type") {
|
||||||
Some("pbkdf2") => {
|
Some("pbkdf2") => {
|
||||||
let hh = jstr(kdf, "hash").and_then(Hash::parse).unwrap_or(Hash::Sha256);
|
let hh = jstr(kdf, "hash")
|
||||||
hh.pbkdf2(pw, &salt, jnum(kdf, "iterations").unwrap_or(1000) as u32, a_ksize)
|
.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")) => {
|
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(
|
let params = match argon2::Params::new(
|
||||||
jnum(kdf, "memory").unwrap_or(65536) as u32,
|
jnum(kdf, "memory").unwrap_or(65536) as u32,
|
||||||
jnum(kdf, "time").unwrap_or(4) as u32,
|
jnum(kdf, "time").unwrap_or(4) as u32,
|
||||||
@@ -384,7 +432,10 @@ fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut out = vec![0u8; a_ksize];
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
@@ -413,6 +464,11 @@ fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Atalho: abre uma janela da origem como LUKS.
|
/// Atalho: abre uma janela da origem como LUKS.
|
||||||
pub fn unlock_window(base: Arc<dyn Source>, start: u64, len: u64, secrets: &[Secret]) -> UnlockResult {
|
pub fn unlock_window(
|
||||||
|
base: Arc<dyn Source>,
|
||||||
|
start: u64,
|
||||||
|
len: u64,
|
||||||
|
secrets: &[Secret],
|
||||||
|
) -> UnlockResult {
|
||||||
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
||||||
}
|
}
|
||||||
@@ -98,10 +98,20 @@ impl Iv {
|
|||||||
/// Cifra aplicada setor a setor.
|
/// Cifra aplicada setor a setor.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum SectorCipher {
|
pub enum SectorCipher {
|
||||||
Xts { k1: AesAny, k2: AesAny, iv: Iv },
|
Xts {
|
||||||
Cbc { key: AesAny, iv: Iv },
|
k1: AesAny,
|
||||||
|
k2: AesAny,
|
||||||
|
iv: Iv,
|
||||||
|
},
|
||||||
|
Cbc {
|
||||||
|
key: AesAny,
|
||||||
|
iv: Iv,
|
||||||
|
},
|
||||||
/// BitLocker AES-CBC com difusor Elephant (chave de dados + chave de ajuste).
|
/// 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]) {
|
fn gf_double(t: &mut [u8; 16]) {
|
||||||
@@ -200,7 +210,13 @@ pub struct Patch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CryptSource {
|
impl CryptSource {
|
||||||
pub fn new(inner: Arc<dyn Source>, cipher: SectorCipher, data_offset: u64, len: u64, sector: u64) -> Self {
|
pub fn new(
|
||||||
|
inner: Arc<dyn Source>,
|
||||||
|
cipher: SectorCipher,
|
||||||
|
data_offset: u64,
|
||||||
|
len: u64,
|
||||||
|
sector: u64,
|
||||||
|
) -> Self {
|
||||||
let sector = sector.max(512);
|
let sector = sector.max(512);
|
||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
@@ -245,7 +261,8 @@ impl CryptSource {
|
|||||||
}
|
}
|
||||||
crate::source::read_exact_at(self.inner.as_ref(), self.data_offset + rel, out)?;
|
crate::source::read_exact_at(self.inner.as_ref(), self.data_offset + rel, out)?;
|
||||||
if rel < self.plain_from {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -342,5 +359,7 @@ pub fn from_hex(s: &str) -> Option<Vec<u8>> {
|
|||||||
if s.len() % 2 != 0 {
|
if s.len() % 2 != 0 {
|
||||||
return None;
|
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()
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,9 @@ pub struct ClusterIndex {
|
|||||||
|
|
||||||
impl ClusterIndex {
|
impl ClusterIndex {
|
||||||
pub fn new(size: u64) -> Self {
|
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) {
|
pub fn set(&mut self, off: u64) {
|
||||||
let c = (off / CLUSTER) as usize;
|
let c = (off / CLUSTER) as usize;
|
||||||
@@ -36,7 +38,10 @@ impl ClusterIndex {
|
|||||||
}
|
}
|
||||||
pub fn get(&self, off: u64) -> bool {
|
pub fn get(&self, off: u64) -> bool {
|
||||||
let c = (off / CLUSTER) as usize;
|
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.
|
/// Examina um trecho lido e marca os clusters candidatos.
|
||||||
pub fn scan_chunk(&mut self, abs: u64, buf: &[u8]) {
|
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<Item = u64> + '_ {
|
pub fn iter_range(&self, from: u64, to: u64) -> impl Iterator<Item = u64> + '_ {
|
||||||
let (a, b) = (from.div_ceil(CLUSTER), to / CLUSTER);
|
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<'_> {
|
impl Pool<'_> {
|
||||||
fn free_at(&self, off: u64, len: u64) -> bool {
|
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).
|
/// Procura FF D9 logo depois do último MCU (até 64 bytes de preenchimento).
|
||||||
fn eoi_after(data: &[u8], pos: usize) -> Option<usize> {
|
fn eoi_after(data: &[u8], pos: usize) -> Option<usize> {
|
||||||
let end = (pos + 64).min(data.len().saturating_sub(1));
|
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.
|
/// 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 read_len = (hit_len as usize).clamp(4096, MAX_FILE);
|
||||||
let Ok(buf) = read_vec(src, off, read_len) else { return Analysis::Unsupported };
|
let Ok(buf) = read_vec(src, off, read_len) else {
|
||||||
let Some(h) = jpegscan::parse_header(&buf) else { return Analysis::Unsupported };
|
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 d = jpegscan::decode(&h, &buf, State::start(&h), u32::MAX, 512);
|
||||||
let disc = jpegscan::first_discontinuity(&h, &d.dc);
|
let disc = jpegscan::first_discontinuity(&h, &d.dc);
|
||||||
if d.stop == Stop::Done && disc.is_none() {
|
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(),
|
Stop::Truncated | Stop::Done => buf.len(),
|
||||||
};
|
};
|
||||||
if let Some(m) = disc {
|
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);
|
stop_pos = stop_pos.min(bad);
|
||||||
}
|
}
|
||||||
let cl = CLUSTER as usize;
|
let cl = CLUSTER as usize;
|
||||||
if off % CLUSTER != 0 || !allow_rebuild {
|
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![];
|
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 best: Option<(usize, u64, State, f64)> = None;
|
||||||
let mut probes = 0usize;
|
let mut probes = 0usize;
|
||||||
for back in 0..4usize {
|
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 bc = frag_start + bc_rel;
|
||||||
let Some(snap) = snaps
|
let Some(snap) = snaps
|
||||||
.iter()
|
.iter()
|
||||||
@@ -155,24 +192,32 @@ pub fn analyze_jpeg(src: &dyn Source, off: u64, hit_len: u64, pool: &Pool, allow
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let mut plane = jpegscan::Plane::new(&h);
|
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 lo = off.saturating_sub(WINDOW_BEFORE);
|
||||||
let hi = cur_base + bc_rel as u64 + WINDOW_AFTER;
|
let hi = cur_base + bc_rel as u64 + WINDOW_AFTER;
|
||||||
let pre = jpegscan::decode(&h, &joined[..bc], State::start(&h), u32::MAX, 0);
|
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![];
|
let mut ranked: Vec<(f64, u64)> = vec![];
|
||||||
for q in pool.index.iter_range(lo, hi) {
|
for q in pool.index.iter_range(lo, hi) {
|
||||||
if q >= cur_base && q < cur_base + bc_rel as u64 + CLUSTER {
|
if q >= cur_base && q < cur_base + bc_rel as u64 + CLUSTER {
|
||||||
continue;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
probes += 1;
|
probes += 1;
|
||||||
if probes > MAX_PROBES {
|
if probes > MAX_PROBES {
|
||||||
break;
|
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));
|
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;
|
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);
|
let all = jpegscan::decode(&h, &joined, State::start(&h), u32::MAX, 0);
|
||||||
if let Some(m) = jpegscan::first_discontinuity(&h, &all.dc) {
|
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 {
|
if bad > bc {
|
||||||
sp = sp.min(bad);
|
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;
|
frag_start = bc;
|
||||||
suspect = sp.max(bc + cl);
|
suspect = sp.max(bc + cl);
|
||||||
}
|
}
|
||||||
let good = pieces.iter().map(|p| p.1).sum::<usize>() + suspect.saturating_sub(frag_start).min(cl * 4) / 512 * 512;
|
let good = pieces.iter().map(|p| p.1).sum::<usize>()
|
||||||
Analysis::Broken { good_len: (stop_pos.min(good.max(512)) / 512 * 512).max(512) as u64 }
|
+ 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 {
|
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
|
/// 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.
|
/// 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<f64> {
|
fn probe_candidate(
|
||||||
|
src: &dyn Source,
|
||||||
|
h: &Header,
|
||||||
|
prefix: &[u8],
|
||||||
|
plane: &jpegscan::Plane,
|
||||||
|
snap: &State,
|
||||||
|
q: u64,
|
||||||
|
bpm_prefix: f64,
|
||||||
|
) -> Option<f64> {
|
||||||
let more = read_vec(src, q, PROBE).ok()?;
|
let more = read_vec(src, q, PROBE).ok()?;
|
||||||
if more.len() < 1024 {
|
if more.len() < 1024 {
|
||||||
return None;
|
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.
|
// 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 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 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();
|
let sc = sum / n as f64 + 20.0 * ratio.ln().abs();
|
||||||
Some(sc)
|
Some(sc)
|
||||||
}
|
}
|
||||||
+119
-27
@@ -55,7 +55,10 @@ fn pread_exact(f: &File, buf: &mut [u8], mut off: u64) -> io::Result<()> {
|
|||||||
while done < buf.len() {
|
while done < buf.len() {
|
||||||
let n = pread(f, &mut buf[done..], off)?;
|
let n = pread(f, &mut buf[done..], off)?;
|
||||||
if n == 0 {
|
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;
|
done += n;
|
||||||
off += n as u64;
|
off += n as u64;
|
||||||
@@ -83,7 +86,9 @@ enum Codec {
|
|||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
enum Kind {
|
enum Kind {
|
||||||
/// Dados guardados como estão; `checksum` indica 4 bytes de Adler-32 no fim.
|
/// Dados guardados como estão; `checksum` indica 4 bytes de Adler-32 no fim.
|
||||||
Stored { checksum: bool },
|
Stored {
|
||||||
|
checksum: bool,
|
||||||
|
},
|
||||||
Packed(Codec),
|
Packed(Codec),
|
||||||
/// Chunk inteiro repetindo estes 8 bytes (EnCase 7 "empty block").
|
/// Chunk inteiro repetindo estes 8 bytes (EnCase 7 "empty block").
|
||||||
Pattern([u8; 8]),
|
Pattern([u8; 8]),
|
||||||
@@ -141,7 +146,11 @@ pub fn next_segment_ext(ext: &str) -> Option<String> {
|
|||||||
format!("{}AA", first as char)
|
format!("{}AA", first as char)
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
let mut i = 2;
|
||||||
loop {
|
loop {
|
||||||
if c[i] < b'Z' {
|
if c[i] < b'Z' {
|
||||||
@@ -206,13 +215,22 @@ impl EwfSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(files: Vec<File>, chunks: Vec<Chunk>, mut info: EwfInfo, size_hint: u64) -> Result<Self> {
|
fn finish(
|
||||||
|
files: Vec<File>,
|
||||||
|
chunks: Vec<Chunk>,
|
||||||
|
mut info: EwfInfo,
|
||||||
|
size_hint: u64,
|
||||||
|
) -> Result<Self> {
|
||||||
if chunks.is_empty() {
|
if chunks.is_empty() {
|
||||||
return Err(Error::msg("imagem EWF sem tabela de chunks"));
|
return Err(Error::msg("imagem EWF sem tabela de chunks"));
|
||||||
}
|
}
|
||||||
let chunk_size = info.chunk_size;
|
let chunk_size = info.chunk_size;
|
||||||
let max = chunks.len() as u64 * 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();
|
info.segments = files.len();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
files,
|
files,
|
||||||
@@ -284,7 +302,11 @@ impl EwfSource {
|
|||||||
.collect();
|
.collect();
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
let (off, comp) = offs[i];
|
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 {
|
if end <= off {
|
||||||
return Err(Error::msg("tabela EWF inconsistente"));
|
return Err(Error::msg("tabela EWF inconsistente"));
|
||||||
}
|
}
|
||||||
@@ -396,11 +418,25 @@ impl EwfSource {
|
|||||||
let kind = if fl & 4 != 0 {
|
let kind = if fl & 4 != 0 {
|
||||||
Kind::Pattern(off.to_le_bytes())
|
Kind::Pattern(off.to_le_bytes())
|
||||||
} else if fl & 1 != 0 {
|
} 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 {
|
} 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());
|
let mut chunks = Vec::with_capacity(slots.len());
|
||||||
for (i, (k, c)) in slots.into_iter().enumerate() {
|
for (i, (k, c)) in slots.into_iter().enumerate() {
|
||||||
if k != i as u64 {
|
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);
|
chunks.push(c);
|
||||||
}
|
}
|
||||||
@@ -457,10 +495,19 @@ impl EwfSource {
|
|||||||
pread_exact(&self.files[c.seg as usize], &mut raw, c.off)?;
|
pread_exact(&self.files[c.seg as usize], &mut raw, c.off)?;
|
||||||
let mut v = Vec::with_capacity(want);
|
let mut v = Vec::with_capacity(want);
|
||||||
let r: io::Result<usize> = match codec {
|
let r: io::Result<usize> = match codec {
|
||||||
Codec::Bzip2 => bzip2::read::BzDecoder::new(&raw[..]).take(want as u64).read_to_end(&mut v),
|
Codec::Bzip2 => bzip2::read::BzDecoder::new(&raw[..])
|
||||||
_ => flate2::read::ZlibDecoder::new(&raw[..]).take(want as u64).read_to_end(&mut v),
|
.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
|
v
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -529,7 +576,9 @@ impl Source for EwfSource {
|
|||||||
fn inflate_all(raw: &[u8]) -> Vec<u8> {
|
fn inflate_all(raw: &[u8]) -> Vec<u8> {
|
||||||
let mut v = vec![];
|
let mut v = vec![];
|
||||||
if raw.len() > 2 && raw[0] == 0x78 {
|
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() {
|
if !v.is_empty() {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
@@ -538,8 +587,13 @@ fn inflate_all(raw: &[u8]) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn utf16_text(b: &[u8]) -> String {
|
fn utf16_text(b: &[u8]) -> String {
|
||||||
let u: Vec<u16> = b.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect();
|
let u: Vec<u16> = b
|
||||||
String::from_utf16_lossy(&u).trim_start_matches('\u{feff}').to_string()
|
.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 {
|
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);
|
let spc: u64 = v.parse().unwrap_or(64);
|
||||||
info.chunk_size = spc * 512;
|
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) => {
|
(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 d = doy - (153 * mp + 2) / 5 + 1;
|
||||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
let y = if m <= 2 { y + 1 } else { y };
|
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`.
|
/// 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)
|
// O segmento está cheio? (reserva espaço para as tabelas e os hashes finais)
|
||||||
let full = |pos: u64, entries: u64| {
|
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<()> {
|
let result = (|| -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
@@ -781,7 +850,9 @@ pub fn write_e01(
|
|||||||
.write(true)
|
.write(true)
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.open(&path)
|
.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);
|
crate::owner::give_to_user(&path);
|
||||||
created.push(path);
|
created.push(path);
|
||||||
let mut w = SegWriter { f, pos: 0 };
|
let mut w = SegWriter { f, pos: 0 };
|
||||||
@@ -794,7 +865,15 @@ pub fn write_e01(
|
|||||||
let hdr = zlib(&header_text(opts, &now), 9);
|
let hdr = zlib(&header_text(opts, &now), 9);
|
||||||
w.section("header", &hdr, false)?;
|
w.section("header", &hdr, false)?;
|
||||||
w.section("header2", &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.
|
// Grupos "sectors" + "table" + "table2" até estourar o limite do segmento.
|
||||||
loop {
|
loop {
|
||||||
@@ -821,7 +900,9 @@ pub fn write_e01(
|
|||||||
let mut o = 0;
|
let mut o = 0;
|
||||||
while o < want {
|
while o < want {
|
||||||
let n = 512.min(want - o);
|
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);
|
b[o..o + n].fill(0);
|
||||||
match rep.bad_ranges.last_mut() {
|
match rep.bad_ranges.last_mut() {
|
||||||
Some(l) if l.0 + l.1 == off + o as u64 => l.1 += n as u64,
|
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);
|
hasher.update(b);
|
||||||
let rel = w.pos - base_off;
|
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 {
|
if opts.compression > 0 && packed.len() < want {
|
||||||
entries.push(rel as u32 | 0x8000_0000);
|
entries.push(rel as u32 | 0x8000_0000);
|
||||||
w.put(&packed)?;
|
w.put(&packed)?;
|
||||||
@@ -888,8 +973,12 @@ pub fn write_e01(
|
|||||||
if chunk_idx >= chunks_total {
|
if chunk_idx >= chunks_total {
|
||||||
// Segmento final: hashes e "done".
|
// Segmento final: hashes e "done".
|
||||||
let h = std::mem::take(&mut hasher).finalize();
|
let h = std::mem::take(&mut hasher).finalize();
|
||||||
let md5: Vec<u8> = (0..16).map(|i| u8::from_str_radix(&h.md5[i * 2..i * 2 + 2], 16).unwrap()).collect();
|
let md5: Vec<u8> = (0..16)
|
||||||
let sha1: Vec<u8> = (0..20).map(|i| u8::from_str_radix(&h.sha1[i * 2..i * 2 + 2], 16).unwrap()).collect();
|
.map(|i| u8::from_str_radix(&h.md5[i * 2..i * 2 + 2], 16).unwrap())
|
||||||
|
.collect();
|
||||||
|
let sha1: Vec<u8> = (0..20)
|
||||||
|
.map(|i| u8::from_str_radix(&h.sha1[i * 2..i * 2 + 2], 16).unwrap())
|
||||||
|
.collect();
|
||||||
let mut hs = md5.clone();
|
let mut hs = md5.clone();
|
||||||
hs.extend_from_slice(&[0; 16]);
|
hs.extend_from_slice(&[0; 16]);
|
||||||
let c = adler32(&hs);
|
let c = adler32(&hs);
|
||||||
@@ -924,6 +1013,9 @@ pub fn write_e01(
|
|||||||
if rep.cancelled {
|
if rep.cancelled {
|
||||||
rep.bytes = chunk_idx * chunk_size;
|
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)
|
Ok(rep)
|
||||||
}
|
}
|
||||||
@@ -7,28 +7,64 @@ use crate::source::{read_vec, Source};
|
|||||||
use crate::types::Confidence;
|
use crate::types::Confidence;
|
||||||
|
|
||||||
fn u16le(b: &[u8], o: usize) -> u64 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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<Hit> {
|
fn hit(len: u64, ext: &'static str, confidence: Confidence) -> Option<Hit> {
|
||||||
Some(Hit { len, ext, confidence })
|
Some(Hit {
|
||||||
|
len,
|
||||||
|
ext,
|
||||||
|
confidence,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Primeiros bytes que disparam os detectores deste módulo.
|
/// Primeiros bytes que disparam os detectores deste módulo.
|
||||||
pub fn might_start(b0: u8) -> bool {
|
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`.
|
/// 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<Hit> {
|
|||||||
if h.starts_with(&[0x1F, 0x8B, 0x08]) {
|
if h.starts_with(&[0x1F, 0x8B, 0x08]) {
|
||||||
return gzip(src, off, max);
|
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);
|
return bzip2(src, off, max);
|
||||||
}
|
}
|
||||||
if h.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) {
|
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<Hit> {
|
|||||||
}
|
}
|
||||||
if h.starts_with(b"MSCF") {
|
if h.starts_with(b"MSCF") {
|
||||||
let len = u32le(h, 8);
|
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"!<arch>\n") {
|
if h.starts_with(b"!<arch>\n") {
|
||||||
return ar(src, off, max);
|
return ar(src, off, max);
|
||||||
@@ -96,7 +137,9 @@ pub fn detect(src: &dyn Source, off: u64, h: &[u8], max: u64) -> Option<Hit> {
|
|||||||
}
|
}
|
||||||
if h.starts_with(b"FORM") && matches!(&h[8..12], b"AIFF" | b"AIFC") {
|
if h.starts_with(b"FORM") && matches!(&h[8..12], b"AIFF" | b"AIFC") {
|
||||||
let len = u32be(h, 4) + 8;
|
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]) {
|
if h.starts_with(&[0x28, 0xB5, 0x2F, 0xFD]) {
|
||||||
return zstd(src, off, max);
|
return zstd(src, off, max);
|
||||||
@@ -113,7 +156,13 @@ pub fn detect(src: &dyn Source, off: u64, h: &[u8], max: u64) -> Option<Hit> {
|
|||||||
// -------------------------------------------------------------- compactadores
|
// -------------------------------------------------------------- compactadores
|
||||||
|
|
||||||
/// Alimenta um descompactador com blocos de `src` até o fim do fluxo; devolve (bytes lidos, saída crc32 opcional).
|
/// 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 d = flate2::Decompress::new(zlib_header && !raw);
|
||||||
let mut pos = start;
|
let mut pos = start;
|
||||||
let mut out = vec![0u8; 1 << 16];
|
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 {
|
while consumed < n {
|
||||||
let before_in = d.total_in();
|
let before_in = d.total_in();
|
||||||
let before_out = d.total_out();
|
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 used = (d.total_in() - before_in) as usize;
|
||||||
let produced = (d.total_out() - before_out) as usize;
|
let produced = (d.total_out() - before_out) as usize;
|
||||||
for &b in &out[..produced] {
|
for &b in &out[..produced] {
|
||||||
crc ^= b as u32;
|
crc ^= b as u32;
|
||||||
for _ in 0..8 {
|
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;
|
total_out += produced as u64;
|
||||||
@@ -178,7 +233,15 @@ fn gzip(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let ok = u32le(&trailer, 0) as u32 == crc && u32le(&trailer, 4) == out_len & 0xFFFF_FFFF;
|
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<Hit> {
|
fn bzip2(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||||
@@ -286,8 +349,13 @@ fn matroska(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
let h = read_vec(src, off, 4096).ok()?;
|
let h = read_vec(src, off, 4096).ok()?;
|
||||||
let (hs, hl, _) = vint(&h[4..])?;
|
let (hs, hl, _) = vint(&h[4..])?;
|
||||||
let mut p = 4 + hl + hs as usize;
|
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 doctype =
|
||||||
let ext = if doctype.contains("webm") { "webm" } else { "mkv" };
|
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] {
|
if h.get(p..p + 4)? != [0x18, 0x53, 0x80, 0x67] {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -307,9 +375,13 @@ fn matroska(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
if e.len() < 6 {
|
if e.len() < 6 {
|
||||||
break;
|
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 _ = id;
|
||||||
let Some((size, sl2, unk)) = vint(&e[idl..]) else { break };
|
let Some((size, sl2, unk)) = vint(&e[idl..]) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
if unk {
|
if unk {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -370,7 +442,17 @@ fn ogg(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
break;
|
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<Hit> {
|
fn mpegts(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||||
@@ -396,7 +478,9 @@ fn mpegts(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let len = n_packets * 188;
|
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<Hit> {
|
fn flv(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||||
@@ -420,14 +504,16 @@ fn flv(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
return None;
|
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<Hit> {
|
fn asf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||||
let h = read_vec(src, off, 30).ok()?;
|
let h = read_vec(src, off, 30).ok()?;
|
||||||
let hdr_size = u64le(&h, 16);
|
let hdr_size = u64le(&h, 16);
|
||||||
let n_objs = u32le(&h, 24);
|
let n_objs = u32le(&h, 24);
|
||||||
if hdr_size < 30 || hdr_size > 1 << 24 {
|
if !(30..=1 << 24).contains(&hdr_size) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let all = read_vec(src, off + 30, (hdr_size - 30) as usize).ok()?;
|
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<Hit> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let size = u64le(&all, p + 16) as usize;
|
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);
|
let fsize = u64le(&all, p + 40);
|
||||||
if fsize >= hdr_size && fsize <= max {
|
if fsize >= hdr_size && fsize <= max {
|
||||||
return hit(fsize, "wmv", Confidence::High);
|
return hit(fsize, "wmv", Confidence::High);
|
||||||
@@ -596,7 +687,9 @@ fn ar(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
return None;
|
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<Hit> {
|
fn swf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||||
@@ -606,11 +699,21 @@ fn swf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if h[0] == b'F' {
|
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
|
// CWS: corpo em zlib a partir do byte 8
|
||||||
let (consumed, _, out) = inflate_len(src, off + 8, max, false, true)?;
|
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
|
// ---------------------------------------------------------- binários e dados
|
||||||
@@ -635,9 +738,23 @@ fn elf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (shoff, shentsize, shnum, phoff, phentsize, phnum) = if is64 {
|
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 {
|
} 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;
|
let mut end = shoff + shentsize * shnum;
|
||||||
// Segmentos de programa podem ir além (binários sem tabela de seções).
|
// 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<Hit> {
|
|||||||
}
|
}
|
||||||
v
|
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 = 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<Hit> {
|
fn tiff(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||||
@@ -665,11 +798,22 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
let le = h[0] == b'I';
|
let le = h[0] == b'I';
|
||||||
let r16 = |b: &[u8], o: usize| -> u64 {
|
let r16 = |b: &[u8], o: usize| -> u64 {
|
||||||
let s = b.get(o..o + 2).unwrap_or(&[0, 0]);
|
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 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]);
|
let s: [u8; 4] = b
|
||||||
if le { u32::from_le_bytes(s) as u64 } else { u32::from_be_bytes(s) as u64 }
|
.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 ifd = r32(&h, 4);
|
||||||
let mut end = 8u64;
|
let mut end = 8u64;
|
||||||
@@ -714,7 +858,11 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
if count == 0 || count > 1 << 20 {
|
if count == 0 || count > 1 << 20 {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
let bytes: Vec<u8> = 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<u8> = 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 {
|
for k in 0..count as usize {
|
||||||
let v = match sz {
|
let v = match sz {
|
||||||
2 => r16(&bytes, k * 2),
|
2 => r16(&bytes, k * 2),
|
||||||
@@ -726,8 +874,20 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
out
|
out
|
||||||
};
|
};
|
||||||
match tag {
|
match tag {
|
||||||
273 | 324 | 513 => strip_off = if tag == 513 { vec![r32(en, 8)] } else { read_vals(count) },
|
273 | 324 | 513 => {
|
||||||
279 | 325 | 514 => strip_cnt = if tag == 514 { vec![r32(en, 8)] } else { read_vals(count) },
|
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 => {
|
330 | 34665 | 34853 | 40965 => {
|
||||||
if ty == 4 && count == 1 {
|
if ty == 4 && count == 1 {
|
||||||
subs.push(value_off);
|
subs.push(value_off);
|
||||||
@@ -739,7 +899,10 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
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) {
|
for (o, c) in strip_off.iter().zip(&strip_cnt) {
|
||||||
end = end.max(o + c);
|
end = end.max(o + c);
|
||||||
}
|
}
|
||||||
@@ -749,13 +912,23 @@ fn tiff(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
end = end.max(o + c);
|
end = end.max(o + c);
|
||||||
}
|
}
|
||||||
seen += 1;
|
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 {
|
if end > max {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let ext = if h.get(8..10) == Some(b"CR") { "cr2" } else { "tif" };
|
let ext = if h.get(8..10) == Some(b"CR") {
|
||||||
(end > 16).then(|| ()).and_then(|_| hit(end, ext, Confidence::Medium))
|
"cr2"
|
||||||
|
} else {
|
||||||
|
"tif"
|
||||||
|
};
|
||||||
|
(end > 16)
|
||||||
|
.then_some(())
|
||||||
|
.and_then(|_| hit(end, ext, Confidence::Medium))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ico(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
fn ico(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||||
@@ -795,7 +968,17 @@ fn regf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
}
|
}
|
||||||
let ok = x == u32le(&h, 0x1FC) as u32;
|
let ok = x == u32le(&h, 0x1FC) as u32;
|
||||||
let len = 4096 + u32le(&h, 0x28);
|
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<Hit> {
|
fn evtx(src: &dyn Source, off: u64) -> Option<Hit> {
|
||||||
@@ -817,8 +1000,14 @@ fn pst(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let ver = u16le(&h, 10);
|
let ver = u16le(&h, 10);
|
||||||
let len = if ver >= 23 { u64le(&h, 0xB8) } else { u32le(&h, 0xA8) };
|
let len = if ver >= 23 {
|
||||||
(len > 0x200 && len <= max).then(|| ()).and_then(|_| hit(len, "pst", Confidence::High))
|
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<Hit> {
|
fn rtf(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||||
@@ -863,7 +1052,11 @@ fn pcap(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
let le = h[0] == 0xD4;
|
let le = h[0] == 0xD4;
|
||||||
let r32 = |b: &[u8], o: usize| -> u64 {
|
let r32 = |b: &[u8], o: usize| -> u64 {
|
||||||
let s: [u8; 4] = b[o..o + 4].try_into().unwrap();
|
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);
|
let snaplen = r32(&h, 16);
|
||||||
if snaplen == 0 || snaplen > 1 << 20 {
|
if snaplen == 0 || snaplen > 1 << 20 {
|
||||||
@@ -896,7 +1089,9 @@ fn pcap(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
return None;
|
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<Hit> {
|
fn pcapng(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||||
@@ -908,7 +1103,11 @@ fn pcapng(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
};
|
};
|
||||||
let r32 = |b: &[u8], o: usize| -> u64 {
|
let r32 = |b: &[u8], o: usize| -> u64 {
|
||||||
let s: [u8; 4] = b[o..o + 4].try_into().unwrap();
|
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 pos = off;
|
||||||
let mut blocks = 0;
|
let mut blocks = 0;
|
||||||
@@ -931,5 +1130,7 @@ fn pcapng(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
|||||||
return None;
|
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))
|
||||||
}
|
}
|
||||||
@@ -9,13 +9,19 @@ use crate::types::{Confidence, DataLoc, Extent, Origin};
|
|||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
fn le16(b: &[u8], o: usize) -> u16 {
|
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 {
|
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 {
|
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;
|
const OBJ_TYPE_MASK: u32 = 0xFFFF;
|
||||||
@@ -99,22 +105,42 @@ impl<'a> Apfs<'a> {
|
|||||||
}
|
}
|
||||||
let ks = key_base + koff;
|
let ks = key_base + koff;
|
||||||
let k = b.get(ks..ks + klen)?.to_vec();
|
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));
|
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.
|
/// 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 stack = vec![(root, 0u32)];
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
while let Some((p, depth)) = stack.pop() {
|
while let Some((p, depth)) = stack.pop() {
|
||||||
if depth > 8 || !seen.insert(p) {
|
if depth > 8 || !seen.insert(p) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(b) = self.checked_block(p) else { continue };
|
let Some(b) = self.checked_block(p) else {
|
||||||
let Some(n) = self.parse_node(&b, fixed_k, fixed_v) else { continue };
|
continue;
|
||||||
|
};
|
||||||
|
let Some(n) = self.parse_node(&b, fixed_k, fixed_v) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if n.level == 0 {
|
if n.level == 0 {
|
||||||
for (k, v) in &n.entries {
|
for (k, v) in &n.entries {
|
||||||
f(k, v);
|
f(k, v);
|
||||||
@@ -130,10 +156,14 @@ impl<'a> Apfs<'a> {
|
|||||||
/// Mapa de objetos: oid -> [(xid, paddr)] ordenado por xid.
|
/// Mapa de objetos: oid -> [(xid, paddr)] ordenado por xid.
|
||||||
fn omap(&self, omap_paddr: u64) -> HashMap<u64, Vec<(u64, u64)>> {
|
fn omap(&self, omap_paddr: u64) -> HashMap<u64, Vec<(u64, u64)>> {
|
||||||
let mut m: HashMap<u64, Vec<(u64, u64)>> = HashMap::new();
|
let mut m: HashMap<u64, Vec<(u64, u64)>> = 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);
|
let tree = le64(&ob, 48);
|
||||||
self.walk_physical(tree, 16, 16, &mut |k, v| {
|
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() {
|
for v in m.values_mut() {
|
||||||
v.sort();
|
v.sort();
|
||||||
@@ -143,7 +173,11 @@ impl<'a> Apfs<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn lookup(m: &HashMap<u64, Vec<(u64, u64)>>, oid: u64, max_xid: u64) -> Option<u64> {
|
fn lookup(m: &HashMap<u64, Vec<(u64, u64)>>, oid: u64, max_xid: u64) -> Option<u64> {
|
||||||
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
|
// ---------------------------------------------------------------- registros
|
||||||
@@ -188,7 +222,7 @@ fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
|
|||||||
mtime: ns(le64(v, 24)),
|
mtime: ns(le64(v, 24)),
|
||||||
atime: ns(le64(v, 40)),
|
atime: ns(le64(v, 40)),
|
||||||
mode: le16(v, 80),
|
mode: le16(v, 80),
|
||||||
size: le64(v, 84).max(0),
|
size: le64(v, 84),
|
||||||
has: true,
|
has: true,
|
||||||
..Default::default()
|
..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 (xt, xs) = (xf[4 + i * 4], le16(xf, 6 + i * 4) as usize);
|
||||||
let data = xf.get(p..p + xs).unwrap_or(&[]);
|
let data = xf.get(p..p + xs).unwrap_or(&[]);
|
||||||
match xt {
|
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),
|
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 => {
|
R_DIR_REC if k.len() > 12 && v.len() >= 18 => {
|
||||||
let nl = (le32(k, 8) & 0x3FF) as usize;
|
let nl = (le32(k, 8) & 0x3FF) as usize;
|
||||||
if let Some(name) = k.get(12..12 + nl) {
|
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);
|
files.entry(le64(v, 0)).or_default().names.insert(id, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R_FILE_EXTENT if k.len() >= 16 && v.len() >= 24 => {
|
R_FILE_EXTENT if k.len() >= 16 && v.len() >= 24 => {
|
||||||
let len = le64(v, 0) & 0x00FF_FFFF_FFFF_FFFF;
|
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<'_> {
|
impl Apfs<'_> {
|
||||||
/// Percorre a árvore de arquivos a partir de uma raiz virtual.
|
/// Percorre a árvore de arquivos a partir de uma raiz virtual.
|
||||||
fn walk_fstree(&self, root_oid: u64, omap: &HashMap<u64, Vec<(u64, u64)>>, xid: u64, f: &mut dyn FnMut(&[u8], &[u8], u64)) {
|
fn walk_fstree(
|
||||||
let Some(rp) = lookup(omap, root_oid, xid) else { return };
|
&self,
|
||||||
|
root_oid: u64,
|
||||||
|
omap: &HashMap<u64, Vec<(u64, u64)>>,
|
||||||
|
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 stack = vec![(rp, 0u32)];
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
while let Some((p, depth)) = stack.pop() {
|
while let Some((p, depth)) = stack.pop() {
|
||||||
if depth > 10 || !seen.insert(p) {
|
if depth > 10 || !seen.insert(p) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(b) = self.checked_block(p) else { continue };
|
let Some(b) = self.checked_block(p) else {
|
||||||
let Some(n) = self.parse_node(&b, 16, 16) else { continue };
|
continue;
|
||||||
|
};
|
||||||
|
let Some(n) = self.parse_node(&b, 16, 16) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if n.level == 0 {
|
if n.level == 0 {
|
||||||
for (k, v) in &n.entries {
|
for (k, v) in &n.entries {
|
||||||
f(k, v, p);
|
f(k, v, p);
|
||||||
@@ -261,7 +318,10 @@ fn path_of(files: &Files, mut id: u64) -> String {
|
|||||||
if id == 2 || id == 1 {
|
if id == 2 || id == 1 {
|
||||||
break;
|
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)) => {
|
Some((p, n)) => {
|
||||||
parts.push(n);
|
parts.push(n);
|
||||||
id = p;
|
id = p;
|
||||||
@@ -285,7 +345,11 @@ fn parse_volume(b: &[u8]) -> Option<Volume> {
|
|||||||
if &b[32..36] != b"APSB" {
|
if &b[32..36] != b"APSB" {
|
||||||
return None;
|
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 {
|
Some(Volume {
|
||||||
name,
|
name,
|
||||||
omap_paddr: le64(b, 128),
|
omap_paddr: le64(b, 128),
|
||||||
@@ -305,7 +369,12 @@ fn open_container<'a>(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec<u
|
|||||||
if !(4096..=65536).contains(&bs) || !bs.is_power_of_two() {
|
if !(4096..=65536).contains(&bs) || !bs.is_power_of_two() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let a = Apfs { src, base, bs, blocks };
|
let a = Apfs {
|
||||||
|
src,
|
||||||
|
base,
|
||||||
|
bs,
|
||||||
|
blocks,
|
||||||
|
};
|
||||||
// O superbloco mais recente está na área de descritores de checkpoint.
|
// O superbloco mais recente está na área de descritores de checkpoint.
|
||||||
let desc_blocks = le32(&b0, 104) as u64;
|
let desc_blocks = le32(&b0, 104) as u64;
|
||||||
let desc_base = le64(&b0, 112);
|
let desc_base = le64(&b0, 112);
|
||||||
@@ -323,8 +392,12 @@ fn open_container<'a>(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec<u
|
|||||||
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
||||||
let _ = part_len;
|
let _ = part_len;
|
||||||
let mut res = VolumeScan::default();
|
let mut res = VolumeScan::default();
|
||||||
let Some((a, nx)) = open_container(ctx.src, base) else { return res };
|
let Some((a, nx)) = open_container(ctx.src, base) else {
|
||||||
let Some(cont_omap_block) = a.checked_block(le64(&nx, 160)) else { return res };
|
return res;
|
||||||
|
};
|
||||||
|
let Some(cont_omap_block) = a.checked_block(le64(&nx, 160)) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
let _ = cont_omap_block;
|
let _ = cont_omap_block;
|
||||||
let container_omap = a.omap(le64(&nx, 160));
|
let container_omap = a.omap(le64(&nx, 160));
|
||||||
let max_fs = le32(&nx, 180).min(100) as usize;
|
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) {
|
if !seen.insert(p) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(b) = a.checked_block(p) else { continue };
|
let Some(b) = a.checked_block(p) else {
|
||||||
let Some(n) = a.parse_node(&b, 0, 0) else { continue };
|
continue;
|
||||||
|
};
|
||||||
|
let Some(n) = a.parse_node(&b, 0, 0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if n.level == 0 {
|
if n.level == 0 {
|
||||||
for (k, val) in &n.entries {
|
for (k, val) in &n.entries {
|
||||||
let hdr = le64(k, 0);
|
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 {
|
for (xid, sblock) in snaps {
|
||||||
let Some(sb) = a.checked_block(sblock) else { continue };
|
let Some(sb) = a.checked_block(sblock) else {
|
||||||
let Some(sv) = parse_volume(&sb) else { continue };
|
continue;
|
||||||
|
};
|
||||||
|
let Some(sv) = parse_volume(&sb) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
a.walk_fstree(sv.root_oid, &omap, xid, &mut |k, val, _| {
|
a.walk_fstree(sv.root_oid, &omap, xid, &mut |k, val, _| {
|
||||||
add_record(&mut stale, &mut stale_ext, 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 Some(b) = a.block(p) else { continue };
|
||||||
let ty = le32(&b, 24) & OBJ_TYPE_MASK;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
if live_nodes.contains(&p) || fletcher64(&b[8..]) != le64(&b, 0) {
|
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();
|
ids.sort();
|
||||||
let mut seen_first: HashSet<u64> = HashSet::new();
|
let mut seen_first: HashSet<u64> = HashSet::new();
|
||||||
for id in ids {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
let rec = &stale[id];
|
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 {
|
if !ino.has || ino.mode & 0o170000 != 0o100000 || ino.size == 0 {
|
||||||
continue;
|
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) {
|
if ex.values().any(|e| e.2 != 0) {
|
||||||
continue; // extensão cifrada
|
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),
|
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 dir = Some(format!("/{}", path_of(&merged, parent)).replace("//", "/"));
|
||||||
let mut extents: Vec<Extent> = vec![];
|
let mut extents: Vec<Extent> = vec![];
|
||||||
let mut pos = 0u64;
|
let mut pos = 0u64;
|
||||||
let mut ok = true;
|
let mut ok = true;
|
||||||
for (&lo, &(len, paddr, _)) in ex {
|
for (&lo, &(len, paddr, _)) in ex {
|
||||||
if lo > pos {
|
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 {
|
if paddr == 0 {
|
||||||
extents.push(Extent { offset: 0, len, sparse: true });
|
extents.push(Extent {
|
||||||
|
offset: 0,
|
||||||
|
len,
|
||||||
|
sparse: true,
|
||||||
|
});
|
||||||
} else if paddr < a.blocks {
|
} else if paddr < a.blocks {
|
||||||
extents.push(Extent::new(a.base + paddr * a.bs, len));
|
extents.push(Extent::new(a.base + paddr * a.bs, len));
|
||||||
} else {
|
} else {
|
||||||
@@ -478,11 +576,17 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let extents = super::fat::trim_extents(extents, ino.size);
|
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) {
|
if !seen_first.insert(first.offset) {
|
||||||
continue;
|
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 ts = |t: i64| (t > 0).then_some(t);
|
||||||
let nf = NewFound {
|
let nf = NewFound {
|
||||||
name,
|
name,
|
||||||
@@ -493,7 +597,11 @@ fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res:
|
|||||||
modified: ts(ino.mtime),
|
modified: ts(ino.mtime),
|
||||||
created: ts(ino.ctime),
|
created: ts(ino.ctime),
|
||||||
accessed: ts(ino.atime),
|
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),
|
note: Some(note),
|
||||||
};
|
};
|
||||||
let f = make_found(ctx, nf);
|
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).
|
/// 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)> {
|
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 cont = a.omap(le64(&nx, 160));
|
||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
for i in 0..le32(&nx, 180).min(100) as usize {
|
for i in 0..le32(&nx, 180).min(100) as usize {
|
||||||
let oid = le64(&nx, 184 + i * 8);
|
let oid = le64(&nx, 184 + i * 8);
|
||||||
let Some(p) = lookup(&cont, oid, u64::MAX) else { continue };
|
let Some(p) = lookup(&cont, oid, u64::MAX) else {
|
||||||
let Some(vb) = a.checked_block(p) else { continue };
|
continue;
|
||||||
|
};
|
||||||
|
let Some(vb) = a.checked_block(p) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let Some(v) = parse_volume(&vb) else { continue };
|
let Some(v) = parse_volume(&vb) else { continue };
|
||||||
if v.encrypted {
|
if v.encrypted {
|
||||||
continue;
|
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 omap = a.omap(v.omap_paddr);
|
||||||
let mut files: Files = HashMap::new();
|
let mut files: Files = HashMap::new();
|
||||||
let mut ext: Extents = 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 {
|
for (&id, f) in &files {
|
||||||
if !f.inode.has || id <= 2 {
|
if !f.inode.has || id <= 2 {
|
||||||
continue;
|
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();
|
out.sort();
|
||||||
|
|||||||
@@ -22,13 +22,19 @@ const METADATA_ITEM: u8 = 169;
|
|||||||
const CHUNK_ITEM: u8 = 228;
|
const CHUNK_ITEM: u8 = 228;
|
||||||
|
|
||||||
fn le16(b: &[u8], o: usize) -> u16 {
|
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 {
|
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 {
|
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 {
|
pub fn crc32c(data: &[u8]) -> u32 {
|
||||||
@@ -38,7 +44,11 @@ pub fn crc32c(data: &[u8]) -> u32 {
|
|||||||
for i in 0..256u32 {
|
for i in 0..256u32 {
|
||||||
let mut c = i;
|
let mut c = i;
|
||||||
for _ in 0..8 {
|
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;
|
t[i as usize] = c;
|
||||||
}
|
}
|
||||||
@@ -59,7 +69,11 @@ pub struct Key {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn key_at(b: &[u8], o: usize) -> 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)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -108,7 +122,10 @@ impl<'a> Fs<'a> {
|
|||||||
}
|
}
|
||||||
let nodesize = le32(&sb, 148) as u64;
|
let nodesize = le32(&sb, 148) as u64;
|
||||||
let sectorsize = le32(&sb, 144) 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;
|
return None;
|
||||||
}
|
}
|
||||||
let mut fsid = [0u8; 16];
|
let mut fsid = [0u8; 16];
|
||||||
@@ -156,9 +173,17 @@ impl<'a> Fs<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn phys(&self, logical: u64) -> Option<u64> {
|
fn phys(&self, logical: u64) -> Option<u64> {
|
||||||
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)?;
|
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<Node> {
|
fn read_node(&self, logical: u64) -> Option<Node> {
|
||||||
@@ -172,7 +197,7 @@ impl<'a> Fs<'a> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let bytenr = le64(raw, 48);
|
let bytenr = le64(raw, 48);
|
||||||
if expect.map_or(false, |e| e != bytenr) {
|
if expect.is_some_and(|e| e != bytenr) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if crc32c(&raw[32..]) != le32(raw, 0) {
|
if crc32c(&raw[32..]) != le32(raw, 0) {
|
||||||
@@ -208,7 +233,12 @@ impl<'a> Fs<'a> {
|
|||||||
}
|
}
|
||||||
NodeKind::Internal(ptrs)
|
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.
|
/// 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);
|
self.walk_inner(root, f, &mut seen, nodes, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn walk_inner(&self, at: u64, f: &mut dyn FnMut(&Key, &[u8]), seen: &mut HashSet<u64>, nodes: &mut dyn FnMut(&Node), depth: u32) {
|
fn walk_inner(
|
||||||
|
&self,
|
||||||
|
at: u64,
|
||||||
|
f: &mut dyn FnMut(&Key, &[u8]),
|
||||||
|
seen: &mut HashSet<u64>,
|
||||||
|
nodes: &mut dyn FnMut(&Node),
|
||||||
|
depth: u32,
|
||||||
|
) {
|
||||||
if depth > 10 || !seen.insert(at) {
|
if depth > 10 || !seen.insert(at) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -254,7 +291,15 @@ fn parse_chunk(d: &[u8], logical: u64) -> Option<(Chunk, usize)> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let phys = (0..nstripes).map(|i| le64(d, 48 + i * 32 + 8)).collect();
|
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
|
// ---------------------------------------------------------------- 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 => {
|
INODE_REF if d.len() >= 10 => {
|
||||||
let nlen = le16(d, 8) as usize;
|
let nlen = le16(d, 8) as usize;
|
||||||
if let Some(n) = d.get(10..10 + nlen) {
|
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 => {
|
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 {
|
let Some(fs) = Fs::open(ctx.src, base) else {
|
||||||
return res;
|
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());
|
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;
|
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 mut stale: Files = HashMap::new();
|
||||||
let ns = fs.nodesize as usize;
|
let ns = fs.nodesize as usize;
|
||||||
let step = fs.sectorsize as usize;
|
let step = fs.sectorsize as usize;
|
||||||
let md_chunks: Vec<Chunk> = fs.chunks.iter().filter(|c| c.ty & 0x4 != 0).cloned().collect();
|
let md_chunks: Vec<Chunk> = fs
|
||||||
|
.chunks
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.ty & 0x4 != 0)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
let total_md: u64 = md_chunks.iter().map(|c| c.len).sum();
|
let total_md: u64 = md_chunks.iter().map(|c| c.len).sum();
|
||||||
let mut done_md = 0u64;
|
let mut done_md = 0u64;
|
||||||
for c in &md_chunks {
|
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 mut off = 0u64;
|
||||||
let block = 8usize << 20;
|
let block = 8usize << 20;
|
||||||
while off < c.len {
|
while off < c.len {
|
||||||
@@ -416,12 +476,17 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
let n = ((c.len - off) as usize).min(block + ns);
|
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;
|
let mut i = 0usize;
|
||||||
while i + ns <= buf.len() && (off + i as u64) < c.len {
|
while i + ns <= buf.len() && (off + i as u64) < c.len {
|
||||||
let logical = c.logical + off + i as u64;
|
let logical = c.logical + off + i as u64;
|
||||||
// cabeçalho barato antes do CRC
|
// 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 let Some(node) = fs.parse_node(&buf[i..i + ns], Some(logical)) {
|
||||||
if is_fs_tree(node.owner) && !live_nodes.contains(&logical) {
|
if is_fs_tree(node.owner) && !live_nodes.contains(&logical) {
|
||||||
if let NodeKind::Leaf(items) = &node.kind {
|
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() {
|
if !e.has_inode || e.mode & 0o170000 != 0o100000 || e.size == 0 || e.extents.is_empty() {
|
||||||
continue;
|
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 dir = {
|
||||||
let p = path_of(owner, parent);
|
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 {
|
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 {
|
} else if any_alloc {
|
||||||
(Confidence::Medium, "versão antiga do Btrfs; alguns blocos ainda constam como alocados (podem ser de outro arquivo)".to_string())
|
(Confidence::Medium, "versão antiga do Btrfs; alguns blocos ainda constam como alocados (podem ser de outro arquivo)".to_string())
|
||||||
} else {
|
} 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 ts = |t: i64| (t > 0).then_some(t);
|
||||||
let nf = NewFound {
|
let nf = NewFound {
|
||||||
@@ -526,7 +605,10 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
|
|||||||
}
|
}
|
||||||
if foff > pos {
|
if foff > pos {
|
||||||
sparse_marks.push(parts.len());
|
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;
|
plain_only = false;
|
||||||
pos = foff;
|
pos = foff;
|
||||||
}
|
}
|
||||||
@@ -541,7 +623,9 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
|
|||||||
c => decode(c, raw, ram)?,
|
c => decode(c, raw, ram)?,
|
||||||
};
|
};
|
||||||
let len = (bytes.len() as u64).min(e.size - foff);
|
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;
|
plain_only = false;
|
||||||
pos = foff + len;
|
pos = foff + len;
|
||||||
continue;
|
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));
|
let num = num.min(e.size.saturating_sub(foff));
|
||||||
if disk == 0 || ty == 2 {
|
if disk == 0 || ty == 2 {
|
||||||
sparse_marks.push(parts.len());
|
sparse_marks.push(parts.len());
|
||||||
parts.push(Part::Run { offset: 0, len: num });
|
parts.push(Part::Run {
|
||||||
|
offset: 0,
|
||||||
|
len: num,
|
||||||
|
});
|
||||||
plain_only = false;
|
plain_only = false;
|
||||||
} else {
|
} else {
|
||||||
let Some(phys) = fs.phys(disk) 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 overlaps = {
|
||||||
let i = allocated.partition_point(|x| x.end() <= disk);
|
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;
|
any_alloc |= overlaps;
|
||||||
if comp == 0 {
|
if comp == 0 {
|
||||||
parts.push(Part::Run { offset: phys + offset, len: num });
|
parts.push(Part::Run {
|
||||||
|
offset: phys + offset,
|
||||||
|
len: num,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
let codec = match comp {
|
let codec = match comp {
|
||||||
1 => PackedCodec::Zlib,
|
1 => PackedCodec::Zlib,
|
||||||
2 => PackedCodec::Zstd,
|
2 => PackedCodec::Zstd,
|
||||||
3 | 4 | 5 | 6 => PackedCodec::Lzo,
|
3..=6 => PackedCodec::Lzo,
|
||||||
_ => {
|
_ => {
|
||||||
all_ok = false;
|
all_ok = false;
|
||||||
break;
|
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;
|
plain_only = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -588,7 +685,16 @@ fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc,
|
|||||||
all_ok = false;
|
all_ok = false;
|
||||||
}
|
}
|
||||||
if plain_only {
|
if plain_only {
|
||||||
let ex: Vec<Extent> = parts.iter().filter_map(|p| if let Part::Run { offset, len } = p { Some(Extent::new(*offset, *len)) } else { None }).collect();
|
let ex: Vec<Extent> = 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));
|
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.
|
// 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<Vec<u8>> {
|
|||||||
1 => {
|
1 => {
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
let mut v = Vec::with_capacity(ram);
|
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)
|
Some(v)
|
||||||
}
|
}
|
||||||
2 => {
|
2 => {
|
||||||
@@ -695,7 +803,11 @@ pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
let h = byte(ip)?;
|
let h = byte(ip)?;
|
||||||
ip += 1;
|
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)?;
|
copy_match(&mut out, dist, len)?;
|
||||||
let next = t & 3;
|
let next = t & 3;
|
||||||
lits(&mut out, &mut ip, next)?;
|
lits(&mut out, &mut ip, next)?;
|
||||||
@@ -708,7 +820,11 @@ pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
|
|||||||
lits(&mut out, &mut ip, next)?;
|
lits(&mut out, &mut ip, next)?;
|
||||||
state = next;
|
state = next;
|
||||||
} else if t >= 32 {
|
} 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);
|
let b = (byte(ip)?) | (byte(ip + 1)? << 8);
|
||||||
ip += 2;
|
ip += 2;
|
||||||
copy_match(&mut out, 1 + (b >> 2), len)?;
|
copy_match(&mut out, 1 + (b >> 2), len)?;
|
||||||
@@ -745,7 +861,9 @@ fn copy_match(out: &mut Vec<u8>, dist: usize, len: usize) -> Option<()> {
|
|||||||
|
|
||||||
/// Arquivos e pastas atuais com caminho completo e tamanho (usa só as árvores vivas).
|
/// 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)> {
|
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![];
|
let mut roots: Vec<(u64, u64)> = vec![];
|
||||||
fs.walk(fs.root, &mut |k, d| {
|
fs.walk(fs.root, &mut |k, d| {
|
||||||
if k.ty == ROOT_ITEM && d.len() >= 239 && is_fs_tree(k.objectid) {
|
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<TreeLocations> {
|
|||||||
root_tree_logical: fs.root,
|
root_tree_logical: fs.root,
|
||||||
fs_tree_logical: fs_t,
|
fs_tree_logical: fs_t,
|
||||||
extent_tree_logical: ext_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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
+193
-55
@@ -18,13 +18,19 @@ const EXT_MAGIC: u16 = 0xF30A;
|
|||||||
const JBD2_MAGIC: u32 = 0xC03B_3998;
|
const JBD2_MAGIC: u32 = 0xC03B_3998;
|
||||||
|
|
||||||
fn le16(b: &[u8], o: usize) -> u16 {
|
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 {
|
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 {
|
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)]
|
#[derive(Clone, Copy, Default)]
|
||||||
@@ -102,7 +108,9 @@ type ExtentList = Vec<(u64, u64, u64, bool)>;
|
|||||||
|
|
||||||
impl<'a> Ext<'a> {
|
impl<'a> Ext<'a> {
|
||||||
fn open(src: &'a dyn Source, base: u64) -> Option<Self> {
|
fn open(src: &'a dyn Source, base: u64) -> Option<Self> {
|
||||||
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 {
|
if le16(&sb, 56) != 0xEF53 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -125,7 +133,11 @@ impl<'a> Ext<'a> {
|
|||||||
if !(128..=4096).contains(&inode_size) {
|
if !(128..=4096).contains(&inode_size) {
|
||||||
return None;
|
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 ngroups = blocks.saturating_sub(first_data).div_ceil(bpg);
|
||||||
let gdt_block = first_data + 1;
|
let gdt_block = first_data + 1;
|
||||||
let gdt = read_vec(src, base + gdt_block * bs, (ngroups * desc_size) as usize).ok()?;
|
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).
|
/// 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 {
|
if node.len() < 12 || le16(node, 0) != EXT_MAGIC {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -219,7 +237,9 @@ impl<'a> Ext<'a> {
|
|||||||
out.push((lblk, start, len, uninit));
|
out.push((lblk, start, len, uninit));
|
||||||
} else {
|
} else {
|
||||||
let child = ((le16(e, 8) as u64) << 32) | le32(e, 4) as u64;
|
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) {
|
if !self.walk_extent_node(&b, depth - 1, out, true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -249,14 +269,12 @@ impl<'a> Ext<'a> {
|
|||||||
v.truncate(max_blocks as usize);
|
v.truncate(max_blocks as usize);
|
||||||
} else {
|
} else {
|
||||||
ind(ptr(12), &mut v);
|
ind(ptr(12), &mut v);
|
||||||
if (v.len() as u64) < max_blocks {
|
if (v.len() as u64) < max_blocks && ptr(13) != 0 {
|
||||||
if ptr(13) != 0 {
|
if let Some(d) = self.read_block(ptr(13)) {
|
||||||
if let Some(d) = self.read_block(ptr(13)) {
|
for i in 0..ppb as usize {
|
||||||
for i in 0..ppb as usize {
|
ind(le32(&d, i * 4) as u64, &mut v);
|
||||||
ind(le32(&d, i * 4) as u64, &mut v);
|
if v.len() as u64 >= max_blocks {
|
||||||
if v.len() as u64 >= max_blocks {
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,14 +329,22 @@ impl<'a> Ext<'a> {
|
|||||||
}
|
}
|
||||||
if l > cur_l {
|
if l > cur_l {
|
||||||
let hole = ((l - cur_l) * self.bs).min(left);
|
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;
|
left -= hole;
|
||||||
if left == 0 {
|
if left == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let take = (n * self.bs).min(left);
|
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;
|
left -= take;
|
||||||
cur_l = l + n;
|
cur_l = l + n;
|
||||||
}
|
}
|
||||||
@@ -340,9 +366,13 @@ impl<'a> Ext<'a> {
|
|||||||
if gr.flags & 0x2 != 0 {
|
if gr.flags & 0x2 != 0 {
|
||||||
used_bits.fill(false);
|
used_bits.fill(false);
|
||||||
free.push(Extent::new(self.base + gstart * self.bs, gblocks * self.bs));
|
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)
|
} else if let Some(bm) = read_vec(
|
||||||
.ok()
|
self.src,
|
||||||
.filter(|b| b.len() as u64 == gblocks.div_ceil(8))
|
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<u64> = None;
|
let mut run: Option<u64> = None;
|
||||||
for i in 0..gblocks {
|
for i in 0..gblocks {
|
||||||
@@ -351,14 +381,20 @@ impl<'a> Ext<'a> {
|
|||||||
match (used, run) {
|
match (used, run) {
|
||||||
(false, None) => run = Some(i),
|
(false, None) => run = Some(i),
|
||||||
(true, Some(s)) => {
|
(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;
|
run = None;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(s) = run {
|
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);
|
allocated_all.extend(used_bits);
|
||||||
@@ -395,7 +431,11 @@ fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
|
|||||||
let real = (8 + nlen).div_ceil(4) * 4;
|
let real = (8 + nlen).div_ceil(4) * 4;
|
||||||
if ino != 0 && nlen > 0 && ino <= max_ino {
|
if ino != 0 && nlen > 0 && ino <= max_ino {
|
||||||
let name = String::from_utf8_lossy(&b[off + 8..off + 8 + nlen]).to_string();
|
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.
|
// Folga: entradas apagadas ficam coladas depois do fim "real" desta.
|
||||||
let mut s = off + if ino != 0 || nlen > 0 { real } else { 8 };
|
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<DirEnt>) {
|
|||||||
&& drec >= 8 + dn
|
&& drec >= 8 + dn
|
||||||
&& drec % 4 == 0
|
&& drec % 4 == 0
|
||||||
&& s + drec <= end
|
&& 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 {
|
if !ok {
|
||||||
s += 4;
|
s += 4;
|
||||||
continue;
|
continue;
|
||||||
@@ -437,16 +479,22 @@ struct Names {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn walk_dirs(ext: &Ext, ctx: &Ctx) -> 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 queue: Vec<(u64, String)> = vec![(2, String::new())];
|
||||||
let mut seen: HashSet<u64> = HashSet::new();
|
let mut seen: HashSet<u64> = HashSet::new();
|
||||||
let mut visited = 0usize;
|
let mut visited = 0usize;
|
||||||
while let Some((dino, path)) = queue.pop() {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
visited += 1;
|
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![];
|
let mut ents = vec![];
|
||||||
for e in ext.file_extents(&ino, ino.size.min(64 << 20)) {
|
for e in ext.file_extents(&ino, ino.size.min(64 << 20)) {
|
||||||
if e.sparse {
|
if e.sparse {
|
||||||
@@ -467,10 +515,16 @@ fn walk_dirs(ext: &Ext, ctx: &Ctx) -> Names {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if en.live {
|
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)));
|
queue.push((en.ino, format!("{}/{}", path, en.name)));
|
||||||
} else {
|
} 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<u64, Vec<Vec<u8>>> {
|
|||||||
if ext.journal_inum == 0 {
|
if ext.journal_inum == 0 {
|
||||||
return out;
|
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));
|
let jext = ext.file_extents(&jino, jino.size.min(1 << 30));
|
||||||
// Blocos físicos do diário em ordem lógica.
|
// Blocos físicos do diário em ordem lógica.
|
||||||
let mut jblocks: Vec<u64> = vec![];
|
let mut jblocks: Vec<u64> = vec![];
|
||||||
@@ -500,7 +556,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
|
|||||||
if jblocks.len() < 4 {
|
if jblocks.len() < 4 {
|
||||||
return out;
|
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 {
|
if be32(&jsb, 0) != JBD2_MAGIC {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -512,13 +570,21 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
|
|||||||
let is64 = incompat & 0x2 != 0;
|
let is64 = incompat & 0x2 != 0;
|
||||||
let csum_v3 = incompat & 0x10 != 0;
|
let csum_v3 = incompat & 0x10 != 0;
|
||||||
let csum_v2 = incompat & 0x8 != 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;
|
let have_csum = csum_v2 || csum_v3;
|
||||||
// Tabela de inodes: faixa de blocos de cada grupo.
|
// Tabela de inodes: faixa de blocos de cada grupo.
|
||||||
let itable_blocks = (ext.ipg * ext.inode_size).div_ceil(ext.bs);
|
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)
|
let mut tables: Vec<(u64, u64, u64)> = vec![]; // (início, fim, primeiro inode)
|
||||||
for (g, gr) in ext.groups.iter().enumerate() {
|
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 n = jblocks.len();
|
||||||
let first = (be32(&jsb, 20) as usize).clamp(1, n);
|
let first = (be32(&jsb, 20) as usize).clamp(1, n);
|
||||||
@@ -540,7 +606,9 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
|
|||||||
}
|
}
|
||||||
seqs.sort_by_key(|s| s.0);
|
seqs.sort_by_key(|s| s.0);
|
||||||
for (_seq, didx) in seqs {
|
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 limit = d.len() - if have_csum { 4 } else { 0 };
|
||||||
let mut off = 12usize;
|
let mut off = 12usize;
|
||||||
let mut data_idx = didx + 1;
|
let mut data_idx = didx + 1;
|
||||||
@@ -552,14 +620,21 @@ fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
|
|||||||
let (flags, hi) = if csum_v3 {
|
let (flags, hi) = if csum_v3 {
|
||||||
(be32(&d, off + 4), be32(&d, off + 8))
|
(be32(&d, off + 4), be32(&d, off + 8))
|
||||||
} else {
|
} 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;
|
blk |= (hi as u64) << 32;
|
||||||
off += tag_bytes;
|
off += tag_bytes;
|
||||||
if flags & 2 == 0 {
|
if flags & 2 == 0 {
|
||||||
off += 16;
|
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;
|
data_idx += 1;
|
||||||
if flags & 4 == 0 {
|
if flags & 4 == 0 {
|
||||||
if let Some(t) = tables.iter().position(|t| blk >= t.0 && blk < t.1) {
|
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<u64, Vec<Vec<u8>>> {
|
|||||||
let per_block = (ext.bs / ext.inode_size) as usize;
|
let per_block = (ext.bs / ext.inode_size) as usize;
|
||||||
let first_ino = tables[t].2 + (blk - tables[t].0) * per_block as u64;
|
let first_ino = tables[t].2 + (blk - tables[t].0) * per_block as u64;
|
||||||
for k in 0..per_block {
|
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) {
|
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
|
continue; // INODE_UNINIT
|
||||||
}
|
}
|
||||||
let used = ext.ipg.saturating_sub(gr.itable_unused as u64).min(ext.ipg);
|
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;
|
continue;
|
||||||
};
|
};
|
||||||
for i in 0..(table.len() as u64 / ext.inode_size) {
|
for i in 0..(table.len() as u64 / ext.inode_size) {
|
||||||
let ino_no = g as u64 * ext.ipg + i + 1;
|
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 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 {
|
if names.live.contains_key(&ino_no) || ino_no < 11 || ino.mode == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -645,7 +730,16 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
|||||||
cand = Some((ino.clone(), "inode"));
|
cand = Some((ino.clone(), "inode"));
|
||||||
}
|
}
|
||||||
if let Some((c, src_kind)) = cand {
|
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);
|
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);
|
let cur = ext.read_inode(*ino_no);
|
||||||
// Se o inode atual está vivo e é outro arquivo, não mexe.
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
for raw in snaps.iter().rev() {
|
for raw in snaps.iter().rev() {
|
||||||
if let Some(j) = Inode::parse(raw) {
|
if let Some(j) = Inode::parse(raw) {
|
||||||
if j.is_reg() && j.size > 0 && j.dtime == 0 && j.links > 0 {
|
if j.is_reg()
|
||||||
if emit_inode(ctx, &ext, &alloc, &names, *ino_no, &j, "diário", &mut seen_extents) {
|
&& j.size > 0
|
||||||
break;
|
&& 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) {
|
if extents.is_empty() || extents.iter().all(|e| e.sparse) {
|
||||||
return false;
|
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)) {
|
if !seen.insert((first.0, first.1)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -735,8 +849,14 @@ fn emit_inode(
|
|||||||
let mut note = Some(format!("recuperado do {src_kind} do ext4"));
|
let mut note = Some(format!("recuperado do {src_kind} do ext4"));
|
||||||
let mut conf = Confidence::High;
|
let mut conf = Confidence::High;
|
||||||
if reused > 0 {
|
if reused > 0 {
|
||||||
conf = if reused * 2 > total_blocks { Confidence::Low } else { Confidence::Medium };
|
conf = if reused * 2 > total_blocks {
|
||||||
note = Some(format!("{reused} de {total_blocks} blocos já foram reaproveitados por outros arquivos"));
|
Confidence::Low
|
||||||
|
} else {
|
||||||
|
Confidence::Medium
|
||||||
|
};
|
||||||
|
note = Some(format!(
|
||||||
|
"{reused} de {total_blocks} blocos já foram reaproveitados por outros arquivos"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if estimated {
|
if estimated {
|
||||||
// Tamanho desconhecido: tenta achar o fim real pelo conteúdo.
|
// Tamanho desconhecido: tenta achar o fim real pelo conteúdo.
|
||||||
@@ -754,7 +874,8 @@ fn emit_inode(
|
|||||||
}
|
}
|
||||||
if estimated {
|
if estimated {
|
||||||
conf = conf.min(Confidence::Medium);
|
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)) {
|
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<Extent> {
|
|||||||
}
|
}
|
||||||
if l > cur {
|
if l > cur {
|
||||||
let hole = ((l - cur) * ext.bs).min(left);
|
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;
|
left -= hole;
|
||||||
if left == 0 {
|
if left == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let take = (n * ext.bs).min(left);
|
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;
|
left -= take;
|
||||||
cur = l + n;
|
cur = l + n;
|
||||||
}
|
}
|
||||||
@@ -841,7 +970,9 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
|
|||||||
if !seen.insert(d) || total >= 600 {
|
if !seen.insert(d) || total >= 600 {
|
||||||
continue;
|
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![];
|
let mut ents = vec![];
|
||||||
for e in ext.file_extents(&ino, ino.size.min(1 << 20)) {
|
for e in ext.file_extents(&ino, ino.size.min(1 << 20)) {
|
||||||
if e.sparse {
|
if e.sparse {
|
||||||
@@ -863,7 +994,10 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
|
|||||||
if ents.iter().any(|e| e.live && e.name == "." && e.ino == d) {
|
if ents.iter().any(|e| e.live && e.name == "." && e.ino == d) {
|
||||||
dots_ok += 1;
|
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;
|
total += 1;
|
||||||
if let Some(i) = ext.read_inode(en.ino) {
|
if let Some(i) = ext.read_inode(en.ino) {
|
||||||
if i.mode != 0 && i.links > 0 {
|
if i.mode != 0 && i.links > 0 {
|
||||||
@@ -882,6 +1016,10 @@ pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let a = ok as f64 / total as f64;
|
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)
|
Some(1.0 + 5.0 * a + 5.0 * b)
|
||||||
}
|
}
|
||||||
@@ -12,13 +12,19 @@ use std::collections::{HashMap, HashSet};
|
|||||||
const HFS_EPOCH: i64 = 2_082_844_800;
|
const HFS_EPOCH: i64 = 2_082_844_800;
|
||||||
|
|
||||||
fn be16(b: &[u8], o: usize) -> u16 {
|
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 {
|
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 {
|
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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
@@ -29,7 +35,11 @@ struct Fork {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_fork(b: &[u8]) -> 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 {
|
for i in 0..8 {
|
||||||
let (s, c) = (be32(b, 16 + i * 8), be32(b, 20 + i * 8));
|
let (s, c) = (be32(b, 16 + i * 8), be32(b, 20 + i * 8));
|
||||||
if c != 0 {
|
if c != 0 {
|
||||||
@@ -105,9 +115,13 @@ impl<'a> Vol<'a> {
|
|||||||
have = ex.iter().map(|e| e.1 as u64).sum();
|
have = ex.iter().map(|e| e.1 as u64).sum();
|
||||||
let _ = have;
|
let _ = have;
|
||||||
}
|
}
|
||||||
merge_extents(ex.into_iter().map(|(s, c)| Extent::new(self.block_off(s as u64), c as u64 * self.bs)).collect::<Vec<_>>())
|
merge_extents(
|
||||||
.into_iter()
|
ex.into_iter()
|
||||||
.collect()
|
.map(|(s, c)| Extent::new(self.block_off(s as u64), c as u64 * self.bs))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extensões além das oito iniciais, procuradas no arquivo de overflow (árvore B+).
|
/// 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;
|
let total_nodes = self.extents_file.size / node_size as u64;
|
||||||
for n in 1..total_nodes.min(100_000) {
|
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 {
|
if node[8] != 0xFF {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -193,8 +209,13 @@ fn records(node: &[u8]) -> Vec<&[u8]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn utf16be(b: &[u8]) -> String {
|
fn utf16be(b: &[u8]) -> String {
|
||||||
let u: Vec<u16> = b.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
|
let u: Vec<u16> = b
|
||||||
String::from_utf16_lossy(&u).replace('/', "_").replace(':', "/")
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
||||||
|
.collect();
|
||||||
|
String::from_utf16_lossy(&u)
|
||||||
|
.replace('/', "_")
|
||||||
|
.replace(':', "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CatFile {
|
struct CatFile {
|
||||||
@@ -213,7 +234,14 @@ fn date(d: u32) -> Option<i64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodifica um registro do catálogo (chave + dados). `max_id` descarta lixo.
|
/// Decodifica um registro do catálogo (chave + dados). `max_id` descarta lixo.
|
||||||
fn decode_record(r: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut HashMap<u32, (u32, String)>, folders: &mut HashMap<(u32, String), u32>, stale: bool) {
|
fn decode_record(
|
||||||
|
r: &[u8],
|
||||||
|
max_id: u32,
|
||||||
|
files: &mut Vec<CatFile>,
|
||||||
|
threads: &mut HashMap<u32, (u32, String)>,
|
||||||
|
folders: &mut HashMap<(u32, String), u32>,
|
||||||
|
stale: bool,
|
||||||
|
) {
|
||||||
let klen = be16(r, 0) as usize;
|
let klen = be16(r, 0) as usize;
|
||||||
if klen < 6 || klen + 2 > r.len() {
|
if klen < 6 || klen + 2 > r.len() {
|
||||||
return;
|
return;
|
||||||
@@ -240,7 +268,10 @@ fn decode_record(r: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut
|
|||||||
// thread de pasta: chave = (id da pasta, ""); dados = (pai, nome)
|
// thread de pasta: chave = (id da pasta, ""); dados = (pai, nome)
|
||||||
let pn = be16(rec, 8) as usize;
|
let pn = be16(rec, 8) as usize;
|
||||||
if pn <= 255 {
|
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 => {
|
2 if rec.len() >= 248 => {
|
||||||
@@ -263,7 +294,10 @@ fn decode_record(r: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut
|
|||||||
4 if rec.len() >= 10 => {
|
4 if rec.len() >= 10 => {
|
||||||
let pn = be16(rec, 8) as usize;
|
let pn = be16(rec, 8) as usize;
|
||||||
if pn <= 255 {
|
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<CatFile>, threads: &mut
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Procura registros de arquivo nas sobras de um nó (entre os dados e a tabela de offsets).
|
/// 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<CatFile>, threads: &mut HashMap<u32, (u32, String)>, folders: &mut HashMap<(u32, String), u32>) {
|
fn scan_slack(
|
||||||
|
node: &[u8],
|
||||||
|
max_id: u32,
|
||||||
|
files: &mut Vec<CatFile>,
|
||||||
|
threads: &mut HashMap<u32, (u32, String)>,
|
||||||
|
folders: &mut HashMap<(u32, String), u32>,
|
||||||
|
) {
|
||||||
let n = be16(node, 10) as usize;
|
let n = be16(node, 10) as usize;
|
||||||
let table = node.len().saturating_sub(2 * (n + 1));
|
let table = node.len().saturating_sub(2 * (n + 1));
|
||||||
let used_end = (be16(node, table) as usize).max(14);
|
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<CatFile>, threads: &mut
|
|||||||
if parent >= 1 && parent < max_id.max(16) {
|
if parent >= 1 && parent < max_id.max(16) {
|
||||||
let d = (p + 2 + klen + 1) & !1;
|
let d = (p + 2 + klen + 1) & !1;
|
||||||
let rt = be16(node, d);
|
let rt = be16(node, d);
|
||||||
if matches!(rt, 1 | 2 | 3 | 4) {
|
if matches!(rt, 1..=4) {
|
||||||
let end = (d + 248).min(table);
|
let end = (d + 248).min(table);
|
||||||
decode_record(&node[p..end], max_id, files, threads, folders, true);
|
decode_record(&node[p..end], max_id, files, threads, folders, true);
|
||||||
p = d + if rt == 2 { 248 } else { 16 };
|
p = d + if rt == 2 { 248 } else { 16 };
|
||||||
@@ -299,15 +339,22 @@ fn scan_slack(node: &[u8], max_id: u32, files: &mut Vec<CatFile>, threads: &mut
|
|||||||
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
||||||
let mut res = VolumeScan::default();
|
let mut res = VolumeScan::default();
|
||||||
let _ = part_len;
|
let _ = part_len;
|
||||||
let Some(vbase) = locate(ctx.src, base) else { return res };
|
let Some(vbase) = locate(ctx.src, base) else {
|
||||||
let Some(vol) = Vol::open(ctx.src, vbase) else { return res };
|
return res;
|
||||||
|
};
|
||||||
|
let Some(vol) = Vol::open(ctx.src, vbase) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
// Mapa de espaço livre.
|
// Mapa de espaço livre.
|
||||||
if let Some(bm) = vol.read_fork_all(&vol.alloc, (vol.total / 8 + 1) as usize) {
|
if let Some(bm) = vol.read_fork_all(&vol.alloc, (vol.total / 8 + 1) as usize) {
|
||||||
let mut free = vec![];
|
let mut free = vec![];
|
||||||
let mut run: Option<u64> = None;
|
let mut run: Option<u64> = None;
|
||||||
for b in 0..vol.total {
|
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) {
|
match (used, run) {
|
||||||
(false, None) => run = Some(b),
|
(false, None) => run = Some(b),
|
||||||
(true, Some(s)) => {
|
(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());
|
res.warnings.push("HFS+: catálogo ilegível.".into());
|
||||||
return res;
|
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 first_leaf = be32(&hdr, 24);
|
||||||
let max_id = vol.next_cnid.max(16);
|
let max_id = vol.next_cnid.max(16);
|
||||||
let mut files: Vec<CatFile> = vec![];
|
let mut files: Vec<CatFile> = vec![];
|
||||||
@@ -342,7 +391,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
|||||||
if (ctx.cancelled)() {
|
if (ctx.cancelled)() {
|
||||||
return res;
|
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 {
|
if node[8] != 0xFF {
|
||||||
break;
|
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.
|
// Diário: cópias antigas de nós folha do catálogo.
|
||||||
if vol.journal_info != 0 {
|
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.
|
// 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 free_set = res.free.clone().unwrap_or_default();
|
||||||
let mut seen: HashSet<(u32, u64)> = HashSet::new();
|
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() {
|
if f.data.size == 0 || f.data.extents.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let ex = vol.fork_extents(&f.data, f.id, 0);
|
let ex = vol.fork_extents(&f.data, f.id, 0);
|
||||||
let total_len: u64 = ex.iter().map(|e| e.len).sum();
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
if !seen.insert((f.id, ex[0].offset)) {
|
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?
|
// Blocos já reaproveitados?
|
||||||
let reused = extents.iter().filter(|e| !covered_by(&free_set, e)).count();
|
let reused = extents.iter().filter(|e| !covered_by(&free_set, e)).count();
|
||||||
let (conf, note) = if reused == 0 {
|
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 {
|
} 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 {
|
let nf = NewFound {
|
||||||
name: f.name.clone(),
|
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 {
|
fn covered_by(free: &[Extent], e: &Extent) -> bool {
|
||||||
let i = free.partition_point(|f| f.end() <= e.offset);
|
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<'_> {
|
impl Vol<'_> {
|
||||||
@@ -434,7 +507,14 @@ impl Vol<'_> {
|
|||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
for &(s, c) in &f.extents {
|
for &(s, c) in &f.extents {
|
||||||
let n = (c as u64 * self.bs) as usize;
|
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 {
|
if out.len() >= max {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -444,8 +524,21 @@ impl Vol<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Procura nós folha do catálogo nas transações guardadas no diário.
|
/// 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<CatFile>, threads: &mut HashMap<u32, (u32, String)>, folders: &mut HashMap<(u32, String), u32>, ctx: &Ctx) {
|
fn scan_journal(
|
||||||
let Some(info) = read_vec(vol.src, vol.block_off(vol.journal_info as u64), 52).ok().filter(|b| b.len() == 52) else { return };
|
vol: &Vol,
|
||||||
|
ns: usize,
|
||||||
|
max_id: u32,
|
||||||
|
files: &mut Vec<CatFile>,
|
||||||
|
threads: &mut HashMap<u32, (u32, String)>,
|
||||||
|
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));
|
let (joff, jsize) = (be64(&info, 36), be64(&info, 44));
|
||||||
if jsize == 0 || jsize > 1 << 30 || vol.base + joff + jsize > vol.src.size() {
|
if jsize == 0 || jsize > 1 << 30 || vol.base + joff + jsize > vol.src.size() {
|
||||||
return;
|
return;
|
||||||
@@ -457,14 +550,19 @@ fn scan_journal(vol: &Vol, ns: usize, max_id: u32, files: &mut Vec<CatFile>, thr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let n = chunk.min(jsize - pos) as usize;
|
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;
|
let mut i = 0;
|
||||||
while i + ns <= buf.len().min(n + ns) && i < n {
|
while i + ns <= buf.len().min(n + ns) && i < n {
|
||||||
let nd = &buf[i..i + ns];
|
let nd = &buf[i..i + ns];
|
||||||
// nó folha do catálogo: kind = -1, altura 1, registros plausíveis
|
// 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 {
|
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);
|
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) {
|
for r in records(nd) {
|
||||||
decode_record(r, max_id, files, threads, folders, true);
|
decode_record(r, max_id, files, threads, folders, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ pub mod exfat;
|
|||||||
pub mod ext;
|
pub mod ext;
|
||||||
pub mod fat;
|
pub mod fat;
|
||||||
pub mod hfsplus;
|
pub mod hfsplus;
|
||||||
pub mod xfs;
|
|
||||||
pub mod ntfs;
|
pub mod ntfs;
|
||||||
|
pub mod xfs;
|
||||||
|
|
||||||
use crate::signatures::{category_for_ext, matches_ext};
|
use crate::signatures::{category_for_ext, matches_ext};
|
||||||
use crate::source::{read_vec, Source};
|
use crate::source::{read_vec, Source};
|
||||||
|
|||||||
+105
-26
@@ -10,13 +10,19 @@ use crate::types::{Confidence, DataLoc, Extent, Origin};
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
fn be16(b: &[u8], o: usize) -> u16 {
|
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 {
|
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 {
|
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> {
|
struct Xfs<'a> {
|
||||||
@@ -46,7 +52,12 @@ impl<'a> Xfs<'a> {
|
|||||||
let agblocks = be32(&sb, 84) as u64;
|
let agblocks = be32(&sb, 84) as u64;
|
||||||
let agcount = be32(&sb, 88) as u64;
|
let agcount = be32(&sb, 88) as u64;
|
||||||
let inodesize = be16(&sb, 104) 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;
|
return None;
|
||||||
}
|
}
|
||||||
let incompat = be32(&sb, 216);
|
let incompat = be32(&sb, 216);
|
||||||
@@ -63,7 +74,11 @@ impl<'a> Xfs<'a> {
|
|||||||
agblklog: sb[124] as u32,
|
agblklog: sb[124] as u32,
|
||||||
sectsize: be16(&sb, 102) as u64,
|
sectsize: be16(&sb, 102) as u64,
|
||||||
v5,
|
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),
|
root_ino: be64(&sb, 56),
|
||||||
dblocks: be64(&sb, 8),
|
dblocks: be64(&sb, 8),
|
||||||
})
|
})
|
||||||
@@ -84,7 +99,8 @@ impl<'a> Xfs<'a> {
|
|||||||
let agno = ino >> shift;
|
let agno = ino >> shift;
|
||||||
let agbno = (ino >> self.inopblog) & ((1 << self.agblklog) - 1);
|
let agbno = (ino >> self.inopblog) & ((1 << self.agblklog) - 1);
|
||||||
let idx = ino & ((1 << self.inopblog) - 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<Vec<u8>> {
|
fn read_ino(&self, ino: u64) -> Option<Vec<u8>> {
|
||||||
@@ -104,7 +120,11 @@ impl<'a> Xfs<'a> {
|
|||||||
fn extents_of(&self, ino: &[u8], max_blocks: u64) -> Vec<(u64, u64, u64)> {
|
fn extents_of(&self, ino: &[u8], max_blocks: u64) -> Vec<(u64, u64, u64)> {
|
||||||
let fmt = ino[5];
|
let fmt = ino[5];
|
||||||
let start = self.core_size(ino);
|
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 lit = &ino[start..fork_end.min(ino.len())];
|
||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
match fmt {
|
match fmt {
|
||||||
@@ -136,7 +156,9 @@ impl<'a> Xfs<'a> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(off) = self.fsb_off(fsb) else { 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") {
|
if b.len() < 24 || !(&b[0..4] == b"BMAP" || &b[0..4] == b"BMA3") {
|
||||||
return;
|
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.
|
/// 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;
|
let mut cur = root;
|
||||||
// desce pela esquerda até a folha
|
// desce pela esquerda até a folha
|
||||||
for lvl in (1..levels).rev() {
|
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 {
|
if b.len() < 24 {
|
||||||
return;
|
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();
|
let mut seen = HashSet::new();
|
||||||
while cur != 0 && cur != u32::MAX as u64 && seen.insert(cur) {
|
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 {
|
if b.len() < 24 {
|
||||||
return;
|
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 {
|
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
||||||
let _ = part_len;
|
let _ = part_len;
|
||||||
let mut res = VolumeScan::default();
|
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.
|
// Espaço livre por grupo e inodes livres.
|
||||||
let mut free: Vec<Extent> = vec![];
|
let mut free: Vec<Extent> = vec![];
|
||||||
@@ -237,7 +273,9 @@ pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
|||||||
free_set.sort();
|
free_set.sort();
|
||||||
let is_free = |fsb: u64, count: u64| -> bool {
|
let is_free = |fsb: u64, count: u64| -> bool {
|
||||||
let i = free_set.partition_point(|&(s, c)| s + c <= fsb);
|
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).
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
let all_free = ext.iter().all(|e| is_free(e.1, e.2));
|
let all_free = ext.iter().all(|e| is_free(e.1, e.2));
|
||||||
let extents: Vec<Extent> = ext.iter().filter_map(|e| x.fsb_off(e.1).map(|o| Extent::new(o, e.2 * x.bs))).collect();
|
let extents: Vec<Extent> = 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) {
|
if extents.is_empty() || !seen_first.insert(extents[0].offset) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut size = total_blocks * x.bs;
|
let mut size = total_blocks * x.bs;
|
||||||
let mut conf = if all_free { Confidence::High } else { Confidence::Low };
|
let mut conf = if all_free {
|
||||||
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() };
|
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
|
// tamanho exato: tenta achar o fim real pelo conteúdo
|
||||||
let mut est = true;
|
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 {
|
if h.len > 0 && h.len <= size {
|
||||||
size = h.len;
|
size = h.len;
|
||||||
est = false;
|
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 extents = super::fat::trim_extents(extents, size);
|
||||||
let low = ino & 0xFFFF_FFFF;
|
let low = ino & 0xFFFF_FFFF;
|
||||||
let (dir, name) = match stale_names.get(&low) {
|
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}")),
|
None => (None, format!("inode_{ino}")),
|
||||||
};
|
};
|
||||||
let ts = |s: u32| (s != 0).then_some(s as i64);
|
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,
|
origin: Origin::Xfs,
|
||||||
data: DataLoc::Extents { extents },
|
data: DataLoc::Extents { extents },
|
||||||
modified: ts(be32(&raw, 40)),
|
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)),
|
accessed: ts(be32(&raw, 32)),
|
||||||
base: conf,
|
base: conf,
|
||||||
note: Some(¬e),
|
note: Some(¬e),
|
||||||
@@ -345,7 +404,9 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
|
|||||||
if !seen.insert(dino) || (ctx.cancelled)() || seen.len() > 200_000 {
|
if !seen.insert(dino) || (ctx.cancelled)() || seen.len() > 200_000 {
|
||||||
continue;
|
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 {
|
if be16(&raw, 2) & 0o170000 != 0o040000 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -365,9 +426,14 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
|
|||||||
}
|
}
|
||||||
let nl = lit[p] as usize;
|
let nl = lit[p] as usize;
|
||||||
p += 3;
|
p += 3;
|
||||||
let name = String::from_utf8_lossy(lit.get(p..p + nl).unwrap_or(&[])).to_string();
|
let name =
|
||||||
|
String::from_utf8_lossy(lit.get(p..p + nl).unwrap_or(&[])).to_string();
|
||||||
p += nl + if x.ftype { 1 } else { 0 };
|
p += nl + if x.ftype { 1 } else { 0 };
|
||||||
let ino = if i8 { be64(lit, p) } else { be32(lit, p) as u64 };
|
let ino = if i8 {
|
||||||
|
be64(lit, p)
|
||||||
|
} else {
|
||||||
|
be32(lit, p) as u64
|
||||||
|
};
|
||||||
p += isz;
|
p += isz;
|
||||||
queue.push((ino, format!("{path}/{name}")));
|
queue.push((ino, format!("{path}/{name}")));
|
||||||
}
|
}
|
||||||
@@ -382,8 +448,12 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for k in 0..cnt.min(256) {
|
for k in 0..cnt.min(256) {
|
||||||
let Some(o) = x.fsb_off(fsb + k) else { continue };
|
let Some(o) = x.fsb_off(fsb + k) else {
|
||||||
let Ok(b) = read_vec(x.src, o, x.bs as usize) else { continue };
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(b) = read_vec(x.src, o, x.bs as usize) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
parse_dir_block(x, &b, dino, &path, &mut queue, &mut stale);
|
parse_dir_block(x, &b, dino, &path, &mut queue, &mut stale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,7 +464,14 @@ fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64
|
|||||||
(paths, stale)
|
(paths, stale)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_dir_block(x: &Xfs, b: &[u8], dino: u64, path: &str, queue: &mut Vec<(u64, String)>, stale: &mut HashMap<u64, (u64, String)>) {
|
fn parse_dir_block(
|
||||||
|
x: &Xfs,
|
||||||
|
b: &[u8],
|
||||||
|
dino: u64,
|
||||||
|
path: &str,
|
||||||
|
queue: &mut Vec<(u64, String)>,
|
||||||
|
stale: &mut HashMap<u64, (u64, String)>,
|
||||||
|
) {
|
||||||
let magic = &b[0..4];
|
let magic = &b[0..4];
|
||||||
let hdr = match magic {
|
let hdr = match magic {
|
||||||
b"XD2B" | b"XD2D" => 16,
|
b"XD2B" | b"XD2D" => 16,
|
||||||
@@ -447,7 +524,9 @@ fn scan_free_region(x: &Xfs, r: &[u8], dir_ino: u64, stale: &mut HashMap<u64, (u
|
|||||||
if nl == 0 || name.iter().any(|&c| c < 0x20 || c == b'/' || c == 0x7F) {
|
if nl == 0 || name.iter().any(|&c| c < 0x20 || c == b'/' || c == 0x7F) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
stale.entry(low).or_insert((dir_ino, String::from_utf8_lossy(name).to_string()));
|
stale
|
||||||
|
.entry(low)
|
||||||
|
.or_insert((dir_ino, String::from_utf8_lossy(name).to_string()));
|
||||||
Some((8 + 1 + nl + if x.ftype { 1 } else { 0 } + 2).div_ceil(8) * 8)
|
Some((8 + 1 + nl + if x.ftype { 1 } else { 0 } + 2).div_ceil(8) * 8)
|
||||||
};
|
};
|
||||||
if r.len() >= 16 {
|
if r.len() >= 16 {
|
||||||
|
|||||||
@@ -77,7 +77,12 @@ pub fn create_image_as(
|
|||||||
) -> Result<ImageReport> {
|
) -> Result<ImageReport> {
|
||||||
match format {
|
match format {
|
||||||
ImageFormat::Raw => create_image(src, dest, cancelled, progress),
|
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 {
|
let opts = crate::aff4::Aff4Options {
|
||||||
case_name: case_number.clone(),
|
case_name: case_number.clone(),
|
||||||
examiner: examiner.clone(),
|
examiner: examiner.clone(),
|
||||||
@@ -85,10 +90,25 @@ pub fn create_image_as(
|
|||||||
evidence_number: evidence_number.clone(),
|
evidence_number: evidence_number.clone(),
|
||||||
..Default::default()
|
..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)
|
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 {
|
let opts = crate::ewf::E01Options {
|
||||||
case_number: case_number.clone(),
|
case_number: case_number.clone(),
|
||||||
description: description.clone(),
|
description: description.clone(),
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ struct Huff {
|
|||||||
|
|
||||||
impl Huff {
|
impl Huff {
|
||||||
fn new(counts: &[u8; 16], vals: Vec<u8>) -> Huff {
|
fn new(counts: &[u8; 16], vals: Vec<u8>) -> 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 code = 0i32;
|
||||||
let mut k = 0i32;
|
let mut k = 0i32;
|
||||||
for l in 1..=16 {
|
for l in 1..=16 {
|
||||||
@@ -103,7 +108,15 @@ pub fn parse_header(d: &[u8]) -> Option<Header> {
|
|||||||
if n == 0 || n > 4 || seg.len() < 6 + n * 3 || w == 0 || h == 0 {
|
if n == 0 || n > 4 || seg.len() < 6 + n * 3 || w == 0 || h == 0 {
|
||||||
return None;
|
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));
|
sof = Some((w, h, comps));
|
||||||
}
|
}
|
||||||
0xC2 | 0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => return None, // progressivo, aritmético etc.
|
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<Header> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
for i in 0..64 {
|
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;
|
q += n;
|
||||||
}
|
}
|
||||||
@@ -155,7 +172,13 @@ pub fn parse_header(d: &[u8]) -> Option<Header> {
|
|||||||
let mut cs = vec![];
|
let mut cs = vec![];
|
||||||
for i in 0..ns {
|
for i in 0..ns {
|
||||||
let t = seg[2 + i * 2];
|
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 hmax = cs.iter().map(|c| c.h).max()? as u32;
|
||||||
let vmax = cs.iter().map(|c| c.v).max()? as u32;
|
let vmax = cs.iter().map(|c| c.v).max()? as u32;
|
||||||
@@ -218,7 +241,15 @@ impl State {
|
|||||||
self.pred[0]
|
self.pred[0]
|
||||||
}
|
}
|
||||||
pub fn start(h: &Header) -> State {
|
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<'_> {
|
impl Bits<'_> {
|
||||||
fn fill(&mut self) {
|
fn fill(&mut self) {
|
||||||
while self.st.bitcnt <= 24 && self.marker.is_none() {
|
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 {
|
if b == 0xFF {
|
||||||
match self.d.get(self.st.pos + 1) {
|
match self.d.get(self.st.pos + 1) {
|
||||||
Some(0) => {
|
Some(0) => {
|
||||||
@@ -283,7 +316,10 @@ impl Bits<'_> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
code = (self.st.bitbuf >> (32 - l)) as i32;
|
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.bitbuf <<= l;
|
||||||
self.st.bitcnt -= l;
|
self.st.bitcnt -= l;
|
||||||
let idx = h.valptr[l as usize] + code - h.mincode[l as usize];
|
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]])>,
|
mut hook: Option<&mut dyn FnMut(u32, &[[i32; 64]])>,
|
||||||
) -> Decoded {
|
) -> Decoded {
|
||||||
let mut lblocks: Vec<[i32; 64]> = Vec::new();
|
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 total = h.total_mcus();
|
||||||
let mut dc_out = Vec::new();
|
let mut dc_out = Vec::new();
|
||||||
let mut snaps = Vec::new();
|
let mut snaps = Vec::new();
|
||||||
@@ -428,7 +468,12 @@ pub fn decode_hook(
|
|||||||
last_snap = b.st.pos;
|
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 {
|
fn fail(b: &Bits, data: &[u8]) -> Stop {
|
||||||
@@ -496,8 +541,9 @@ pub fn first_discontinuity(h: &Header, dc: &[i32]) -> Option<usize> {
|
|||||||
// --------------------------------------------------------------- pixels
|
// --------------------------------------------------------------- pixels
|
||||||
|
|
||||||
const ZIGZAG: [usize; 64] = [
|
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,
|
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,
|
||||||
51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
|
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] {
|
fn cos_table() -> &'static [[f32; 8]; 8] {
|
||||||
@@ -506,8 +552,14 @@ fn cos_table() -> &'static [[f32; 8]; 8] {
|
|||||||
let mut t = [[0f32; 8]; 8];
|
let mut t = [[0f32; 8]; 8];
|
||||||
for x in 0..8 {
|
for x in 0..8 {
|
||||||
for u in 0..8 {
|
for u in 0..8 {
|
||||||
let cu = if u == 0 { std::f32::consts::FRAC_1_SQRT_2 } else { 1.0 };
|
let cu = if u == 0 {
|
||||||
t[x][u] = 0.5 * cu * (((2 * x + 1) as f32 * u as f32 * std::f32::consts::PI) / 16.0).cos();
|
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
|
t
|
||||||
@@ -556,7 +608,8 @@ pub fn mcu_luma(h: &Header, blocks: &[[i32; 64]]) -> Vec<u8> {
|
|||||||
let px = idct_block(blk, &h.qt[c.tq]);
|
let px = idct_block(blk, &h.qt[c.tq]);
|
||||||
let (bx, by) = (i % bw, i / bw);
|
let (bx, by) = (i % bw, i / bw);
|
||||||
for y in 0..8 {
|
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
|
out
|
||||||
@@ -576,7 +629,14 @@ impl Plane {
|
|||||||
pub fn new(h: &Header) -> Plane {
|
pub fn new(h: &Header) -> Plane {
|
||||||
let c = &h.comps[0];
|
let c = &h.comps[0];
|
||||||
let (mw, mh) = (c.h as usize * 8, c.v as usize * 8);
|
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]) {
|
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);
|
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.
|
/// 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 mut hook = |mcu: u32, blocks: &[[i32; 64]]| {
|
||||||
let l = mcu_luma(h, blocks);
|
let l = mcu_luma(h, blocks);
|
||||||
plane.put(h, mcu, &l);
|
plane.put(h, mcu, &l);
|
||||||
|
|||||||
@@ -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.
|
//! Guará: núcleo de recuperação de arquivos.
|
||||||
//!
|
//!
|
||||||
//! Princípio central: a origem é aberta **somente para leitura**; nada neste
|
//! Princípio central: a origem é aberta **somente para leitura**; nada neste
|
||||||
@@ -6,9 +13,9 @@
|
|||||||
pub mod aff4;
|
pub mod aff4;
|
||||||
pub mod assemble;
|
pub mod assemble;
|
||||||
pub mod carve;
|
pub mod carve;
|
||||||
|
pub mod crypt;
|
||||||
pub mod defrag;
|
pub mod defrag;
|
||||||
pub mod device;
|
pub mod device;
|
||||||
pub mod crypt;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod ewf;
|
pub mod ewf;
|
||||||
pub mod explorer;
|
pub mod explorer;
|
||||||
@@ -26,9 +33,9 @@ pub mod mp4rebuild;
|
|||||||
pub mod notify;
|
pub mod notify;
|
||||||
pub mod owner;
|
pub mod owner;
|
||||||
pub mod partition;
|
pub mod partition;
|
||||||
pub mod reconnect;
|
|
||||||
pub mod raid;
|
pub mod raid;
|
||||||
pub mod raid_detect;
|
pub mod raid_detect;
|
||||||
|
pub mod reconnect;
|
||||||
pub mod recover;
|
pub mod recover;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
pub mod scan;
|
pub mod scan;
|
||||||
|
|||||||
+134
-30
@@ -60,7 +60,9 @@ pub fn read_pv(src: &dyn Source) -> Option<PvInfo> {
|
|||||||
}
|
}
|
||||||
let mut best: Option<PvInfo> = None;
|
let mut best: Option<PvInfo> = None;
|
||||||
for (mda_off, mda_size) in mdas {
|
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 {
|
if h.len() < 512 || &h[4..20] != MDA_MAGIC {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -85,11 +87,25 @@ pub fn read_pv(src: &dyn Source) -> Option<PvInfo> {
|
|||||||
text = [a, b].concat();
|
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(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 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) {
|
if best.as_ref().map(|b| cand.seqno > b.seqno).unwrap_or(true) {
|
||||||
best = Some(cand);
|
best = Some(cand);
|
||||||
}
|
}
|
||||||
@@ -133,7 +149,10 @@ impl Section {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn sub(&self, name: &str) -> Option<&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<'_> {
|
impl Lexer<'_> {
|
||||||
fn skip(&mut self) {
|
fn skip(&mut self) {
|
||||||
loop {
|
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;
|
self.i += 1;
|
||||||
}
|
}
|
||||||
if self.i < self.b.len() && self.b[self.i] == b'#' {
|
if self.i < self.b.len() && self.b[self.i] == b'#' {
|
||||||
@@ -160,7 +181,13 @@ impl Lexer<'_> {
|
|||||||
fn ident(&mut self) -> Option<String> {
|
fn ident(&mut self) -> Option<String> {
|
||||||
self.skip();
|
self.skip();
|
||||||
let s = self.i;
|
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 += 1;
|
||||||
}
|
}
|
||||||
(self.i > s).then(|| String::from_utf8_lossy(&self.b[s..self.i]).to_string())
|
(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;
|
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;
|
self.i += 1;
|
||||||
}
|
}
|
||||||
if self.i == s {
|
if self.i == s {
|
||||||
return None;
|
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<Section> {
|
pub fn parse(text: &str) -> Option<Section> {
|
||||||
Lexer { b: text.as_bytes(), i: 0 }.section(true)
|
Lexer {
|
||||||
|
b: text.as_bytes(),
|
||||||
|
i: 0,
|
||||||
|
}
|
||||||
|
.section(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------- montagem
|
// ------------------------------------------------------------- montagem
|
||||||
@@ -246,15 +283,32 @@ fn dashed(u: &str) -> String {
|
|||||||
if u.len() != 32 {
|
if u.len() != 32 {
|
||||||
return u.to_string();
|
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)]
|
#[derive(Clone)]
|
||||||
enum Map {
|
enum Map {
|
||||||
/// Lê de `src` a partir de `base` (linear).
|
/// Lê de `src` a partir de `base` (linear).
|
||||||
Linear { src: Option<Arc<dyn Source>>, base: u64 },
|
Linear {
|
||||||
Striped { srcs: Vec<(Option<Arc<dyn Source>>, u64)>, stripe: u64 },
|
src: Option<Arc<dyn Source>>,
|
||||||
Mirror { legs: Vec<(Option<Arc<dyn Source>>, u64)> },
|
base: u64,
|
||||||
|
},
|
||||||
|
Striped {
|
||||||
|
srcs: Vec<(Option<Arc<dyn Source>>, u64)>,
|
||||||
|
stripe: u64,
|
||||||
|
},
|
||||||
|
Mirror {
|
||||||
|
legs: Vec<(Option<Arc<dyn Source>>, u64)>,
|
||||||
|
},
|
||||||
Raid(Arc<RaidSource>),
|
Raid(Arc<RaidSource>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,31 +449,53 @@ impl Ctx<'_> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if srcs.iter().any(|(s, _)| s.is_none()) {
|
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 {
|
if srcs.len() == 1 {
|
||||||
let (src, base) = srcs.into_iter().next().unwrap();
|
let (src, base) = srcs.into_iter().next().unwrap();
|
||||||
Map::Linear { src, base }
|
Map::Linear { src, base }
|
||||||
} else {
|
} 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" => {
|
"mirror" => {
|
||||||
let st = mk_stripes(seg.list("mirrors").unwrap_or(&[]));
|
let st = mk_stripes(seg.list("mirrors").unwrap_or(&[]));
|
||||||
let legs = st
|
let legs = st
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
Map::Mirror { legs }
|
Map::Mirror { legs }
|
||||||
}
|
}
|
||||||
t if t.starts_with("raid") => {
|
t if t.starts_with("raid") => {
|
||||||
let raids = seg.list("raids").unwrap_or(&[]);
|
let raids = seg.list("raids").unwrap_or(&[]);
|
||||||
let imgs: Vec<String> = raids.iter().filter_map(|v| if let Val::Str(s) = v { Some(s.clone()) } else { None }).collect();
|
let imgs: Vec<String> = 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.
|
// 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<Option<Member>> = vec![];
|
let mut members: Vec<Option<Member>> = vec![];
|
||||||
for n in &data_lvs {
|
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 chunk = seg.int("stripe_size").unwrap_or(128) * 512;
|
||||||
let (level, layout) = match t {
|
let (level, layout) = match t {
|
||||||
@@ -435,7 +511,8 @@ impl Ctx<'_> {
|
|||||||
"raid6_nc" | "raid6_n_6" => (Level::R6, Layout::ParityLast),
|
"raid6_nc" | "raid6_n_6" => (Level::R6, Layout::ParityLast),
|
||||||
"raid10" => (Level::R10, Layout::LeftSymmetric),
|
"raid10" => (Level::R10, Layout::LeftSymmetric),
|
||||||
other => {
|
other => {
|
||||||
self.warnings.push(format!("{name}: tipo {other} não suportado"));
|
self.warnings
|
||||||
|
.push(format!("{name}: tipo {other} não suportado"));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -451,7 +528,9 @@ impl Ctx<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
other => {
|
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;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -474,27 +553,48 @@ pub fn assemble_vgs(pvs: Vec<(Arc<dyn Source>, PvInfo)>) -> Vec<VgResult> {
|
|||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
for (vg_name, list) in by_vg {
|
for (vg_name, list) in by_vg {
|
||||||
let newest = list.iter().max_by_key(|(_, p)| p.seqno).unwrap();
|
let newest = list.iter().max_by_key(|(_, p)| p.seqno).unwrap();
|
||||||
let Some(tree) = parse(&newest.1.text) else { continue };
|
let Some(tree) = parse(&newest.1.text) else {
|
||||||
let Some(vg) = tree.sub(&vg_name) else { continue };
|
continue;
|
||||||
|
};
|
||||||
|
let Some(vg) = tree.sub(&vg_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let ext = vg.int("extent_size").unwrap_or(8192) * 512;
|
let ext = vg.int("extent_size").unwrap_or(8192) * 512;
|
||||||
let mut pvmap = BTreeMap::new();
|
let mut pvmap = BTreeMap::new();
|
||||||
let mut warnings = vec![];
|
let mut warnings = vec![];
|
||||||
if let Some(pvsec) = vg.sub("physical_volumes") {
|
if let Some(pvsec) = vg.sub("physical_volumes") {
|
||||||
for (pvname, def) in &pvsec.sections {
|
for (pvname, def) in &pvsec.sections {
|
||||||
let id = def.str("id").unwrap_or("").to_string();
|
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() {
|
if found.is_none() {
|
||||||
warnings.push(format!("volume físico {pvname} ({id}) não foi fornecido"));
|
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 Some(lvsec) = vg.sub("logical_volumes") else {
|
||||||
let mut ctx = Ctx { ext, pvs: pvmap, lvs: lvsec, cache: BTreeMap::new(), warnings };
|
continue;
|
||||||
|
};
|
||||||
|
let mut ctx = Ctx {
|
||||||
|
ext,
|
||||||
|
pvs: pvmap,
|
||||||
|
lvs: lvsec,
|
||||||
|
cache: BTreeMap::new(),
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
let mut lvs = vec![];
|
let mut lvs = vec![];
|
||||||
for (name, def) in &lvsec.sections {
|
for (name, def) in &lvsec.sections {
|
||||||
// Imagens internas do RAID e metadados não são volumes de usuário.
|
// 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 {
|
if hidden {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -502,7 +602,11 @@ pub fn assemble_vgs(pvs: Vec<(Arc<dyn Source>, PvInfo)>) -> Vec<VgResult> {
|
|||||||
lvs.push((name.clone(), src));
|
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
|
out
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,9 @@
|
|||||||
use crate::source::{read_vec, Source};
|
use crate::source::{read_vec, Source};
|
||||||
|
|
||||||
fn be32(b: &[u8], o: usize) -> u32 {
|
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
|
// ------------------------------------------------------------------ bits
|
||||||
@@ -45,7 +47,11 @@ impl<'a> Bits<'a> {
|
|||||||
}
|
}
|
||||||
fn se(&mut self) -> Option<i32> {
|
fn se(&mut self) -> Option<i32> {
|
||||||
let k = self.ue()?;
|
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) {
|
fn skip(&mut self, n: usize) {
|
||||||
self.pos += n;
|
self.pos += n;
|
||||||
@@ -101,10 +107,18 @@ pub fn parse_sps264(nal: &[u8]) -> Option<Sps264> {
|
|||||||
}
|
}
|
||||||
let rbsp = unescape(&nal[1..]);
|
let rbsp = unescape(&nal[1..]);
|
||||||
let mut b = Bits::new(&rbsp);
|
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
|
b.ue()?; // sps_id
|
||||||
let mut chroma = 1;
|
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()?;
|
chroma = b.ue()?;
|
||||||
if chroma == 3 {
|
if chroma == 3 {
|
||||||
b.bit()?;
|
b.bit()?;
|
||||||
@@ -161,10 +175,8 @@ pub fn parse_sps264(nal: &[u8]) -> Option<Sps264> {
|
|||||||
s.height = ((2 - s.frame_mbs_only as u32) * h * 16).checked_sub((ct + cb) * sub_h)?;
|
s.height = ((2 - s.frame_mbs_only as u32) * h * 16).checked_sub((ct + cb) * sub_h)?;
|
||||||
// VUI até o timing_info.
|
// VUI até o timing_info.
|
||||||
if b.bit()? == 1 {
|
if b.bit()? == 1 {
|
||||||
if b.bit()? == 1 {
|
if b.bit()? == 1 && b.u(8)? == 255 {
|
||||||
if b.u(8)? == 255 {
|
b.skip(32);
|
||||||
b.skip(32);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if b.bit()? == 1 {
|
if b.bit()? == 1 {
|
||||||
b.bit()?;
|
b.bit()?;
|
||||||
@@ -207,10 +219,8 @@ fn slice264(nal: &[u8], sps: &Sps264, pic_order_present: bool) -> Option<SliceIn
|
|||||||
b.ue()?; // pps_id
|
b.ue()?; // pps_id
|
||||||
let frame_num = b.u(sps.log2_max_frame_num)?;
|
let frame_num = b.u(sps.log2_max_frame_num)?;
|
||||||
let mut bottom = false;
|
let mut bottom = false;
|
||||||
if !sps.frame_mbs_only {
|
if !sps.frame_mbs_only && b.bit()? == 1 {
|
||||||
if b.bit()? == 1 {
|
bottom = b.bit()? == 1;
|
||||||
bottom = b.bit()? == 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let idr = nal[0] & 0x1F == 5;
|
let idr = nal[0] & 0x1F == 5;
|
||||||
if idr {
|
if idr {
|
||||||
@@ -221,14 +231,22 @@ fn slice264(nal: &[u8], sps: &Sps264, pic_order_present: bool) -> Option<SliceIn
|
|||||||
poc_lsb = Some(b.u(sps.log2_max_poc_lsb)?);
|
poc_lsb = Some(b.u(sps.log2_max_poc_lsb)?);
|
||||||
let _ = pic_order_present;
|
let _ = pic_order_present;
|
||||||
}
|
}
|
||||||
Some(SliceInfo { frame_num, first_mb0: first == 0, idr, poc_lsb, bottom })
|
Some(SliceInfo {
|
||||||
|
frame_num,
|
||||||
|
first_mb0: first == 0,
|
||||||
|
idr,
|
||||||
|
poc_lsb,
|
||||||
|
bottom,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bottom_field_pic_order_in_frame_present_flag do PPS.
|
/// bottom_field_pic_order_in_frame_present_flag do PPS.
|
||||||
fn pps_pic_order_present(nal: &[u8]) -> bool {
|
fn pps_pic_order_present(nal: &[u8]) -> bool {
|
||||||
let rbsp = unescape(&nal[1..]);
|
let rbsp = unescape(&nal[1..]);
|
||||||
let mut b = Bits::new(&rbsp);
|
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)
|
b.bit() == Some(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +363,12 @@ pub fn config_from_mp4(src: &dyn Source, off: u64, max: u64) -> Option<MediaConf
|
|||||||
p += 2 + l;
|
p += 2 + l;
|
||||||
}
|
}
|
||||||
if !sps.is_empty() && !pps.is_empty() {
|
if !sps.is_empty() && !pps.is_empty() {
|
||||||
return Some(MediaConfig { codec: Codec::Avc, sps, pps, vps: vec![] });
|
return Some(MediaConfig {
|
||||||
|
codec: Codec::Avc,
|
||||||
|
sps,
|
||||||
|
pps,
|
||||||
|
vps: vec![],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if tag == b"hvcC" {
|
} else if tag == b"hvcC" {
|
||||||
@@ -371,7 +394,12 @@ pub fn config_from_mp4(src: &dyn Source, off: u64, max: u64) -> Option<MediaConf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !sps.is_empty() && !pps.is_empty() {
|
if !sps.is_empty() && !pps.is_empty() {
|
||||||
return Some(MediaConfig { codec: Codec::Hevc, sps, pps, vps });
|
return Some(MediaConfig {
|
||||||
|
codec: Codec::Hevc,
|
||||||
|
sps,
|
||||||
|
pps,
|
||||||
|
vps,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,10 +476,16 @@ pub fn rebuild(src: &dyn Source, off: u64, avail: u64, refs: &[MediaConfig]) ->
|
|||||||
if let Some(r) = rebuild_with(src, off, avail, None) {
|
if let Some(r) = rebuild_with(src, off, avail, None) {
|
||||||
return Some(r);
|
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<Rebuilt> {
|
fn rebuild_with(
|
||||||
|
src: &dyn Source,
|
||||||
|
off: u64,
|
||||||
|
avail: u64,
|
||||||
|
preset: Option<&MediaConfig>,
|
||||||
|
) -> Option<Rebuilt> {
|
||||||
// Átomos antes do mdat.
|
// Átomos antes do mdat.
|
||||||
let mut p = 0u64;
|
let mut p = 0u64;
|
||||||
let mut pre_end = None;
|
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);
|
pre_end = Some(p);
|
||||||
break;
|
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;
|
return None;
|
||||||
}
|
}
|
||||||
let sz = be32(&h, 0) as u64;
|
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 pre_len = pre_end?;
|
||||||
let (data_start, declared) = mdat_at(src, off + pre_len)?;
|
let (data_start, declared) = mdat_at(src, off + pre_len)?;
|
||||||
let hard_end = off + avail.min(src.size() - off);
|
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.
|
// Lê o payload em blocos, interpretando NALs.
|
||||||
let mut samples: Vec<Sample> = vec![];
|
let mut samples: Vec<Sample> = 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 mut cache_off = data_start;
|
||||||
let read_at = |o: u64, n: usize, cache: &mut Vec<u8>, cache_off: &mut u64| -> Option<Vec<u8>> {
|
let read_at = |o: u64, n: usize, cache: &mut Vec<u8>, cache_off: &mut u64| -> Option<Vec<u8>> {
|
||||||
if o < *cache_off || o + n as u64 > *cache_off + cache.len() as u64 {
|
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 = read_vec(src, o, want).ok()?;
|
||||||
*cache_off = o;
|
*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.
|
// Detecta o codec: o que forma a cadeia mais longa de unidades NAL válidas.
|
||||||
let chain = |c: Codec| -> usize {
|
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 p = 0usize;
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
while p + 6 <= head.len() {
|
while p + 6 <= head.len() {
|
||||||
let l = be32(&head, p) as usize;
|
let l = be32(&head, p) as usize;
|
||||||
let ok = l >= 1 && l <= 8 << 20 && match c {
|
let ok = (1..=8 << 20).contains(&l)
|
||||||
Codec::Avc => avc_nal_ok(head[p + 4]),
|
&& match c {
|
||||||
Codec::Hevc => hevc_nal_ok(head[p + 4], head[p + 5]),
|
Codec::Avc => avc_nal_ok(head[p + 4]),
|
||||||
};
|
Codec::Hevc => hevc_nal_ok(head[p + 4], head[p + 5]),
|
||||||
|
};
|
||||||
if !ok {
|
if !ok {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -540,7 +587,11 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon
|
|||||||
match codec {
|
match codec {
|
||||||
Codec::Avc => {
|
Codec::Avc => {
|
||||||
sps264 = c.sps.iter().find_map(|n| parse_sps264(n));
|
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)),
|
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 {
|
'outer: while pos + 6 <= data_end_decl {
|
||||||
let hdr = read_at(pos, 6, &mut cache, &mut cache_off)?;
|
let hdr = read_at(pos, 6, &mut cache, &mut cache_off)?;
|
||||||
let len = be32(&hdr, 0) as u64;
|
let len = be32(&hdr, 0) as u64;
|
||||||
let ok = len >= 1 && len <= 16 << 20 && pos + 4 + len <= data_end_decl && match codec {
|
let ok = (1..=16 << 20).contains(&len)
|
||||||
Codec::Avc => avc_nal_ok(hdr[4]),
|
&& pos + 4 + len <= data_end_decl
|
||||||
Codec::Hevc => hevc_nal_ok(hdr[4], hdr[5]),
|
&& match codec {
|
||||||
};
|
Codec::Avc => avc_nal_ok(hdr[4]),
|
||||||
|
Codec::Hevc => hevc_nal_ok(hdr[4], hdr[5]),
|
||||||
|
};
|
||||||
let mut ok = ok;
|
let mut ok = ok;
|
||||||
if ok && nal_count >= 3 && len > (max_len * 6).max(1 << 20) {
|
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).
|
// 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 {
|
if !valid {
|
||||||
// Pode ser áudio intercalado: procura a próxima unidade NAL plausível.
|
// 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 {
|
match found {
|
||||||
Some(q) if nal_count > 0 => {
|
Some(q) if nal_count > 0 => {
|
||||||
// O que veio antes fecha o quadro atual.
|
// 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_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 {
|
let (is_vcl, first_of_pic, is_idr, ty) = match codec {
|
||||||
Codec::Avc => {
|
Codec::Avc => {
|
||||||
let t = nal[0] & 0x1F;
|
let t = nal[0] & 0x1F;
|
||||||
match t {
|
match t {
|
||||||
1 | 2 | 5 => {
|
1 | 2 | 5 => {
|
||||||
let first = sps264.as_ref().and_then(|s| slice264(&nal, s, pic_order_present));
|
let first = sps264
|
||||||
(true, first.as_ref().map(|f| f.first_mb0).unwrap_or(true), t == 5, t)
|
.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 => {
|
7 => {
|
||||||
if sps264.is_none() {
|
if sps264.is_none() {
|
||||||
sps264 = parse_sps264(&nal);
|
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());
|
sps_nals.push(nal.clone());
|
||||||
}
|
}
|
||||||
(false, false, false, t)
|
(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() {
|
if pps_nals.is_empty() {
|
||||||
pic_order_present = pps_pic_order_present(&nal);
|
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());
|
pps_nals.push(nal.clone());
|
||||||
}
|
}
|
||||||
(false, false, false, t)
|
(false, false, false, t)
|
||||||
@@ -626,9 +700,14 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon
|
|||||||
Codec::Hevc => {
|
Codec::Hevc => {
|
||||||
let t = (nal[0] >> 1) & 0x3F;
|
let t = (nal[0] >> 1) & 0x3F;
|
||||||
match t {
|
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 => {
|
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());
|
vps_nals.push(nal.clone());
|
||||||
}
|
}
|
||||||
(false, false, false, t)
|
(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() {
|
if sps265.is_none() {
|
||||||
sps265 = parse_sps_hevc(&nal);
|
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());
|
sps_nals.push(nal.clone());
|
||||||
}
|
}
|
||||||
(false, false, false, t)
|
(false, false, false, t)
|
||||||
}
|
}
|
||||||
34 => {
|
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());
|
pps_nals.push(nal.clone());
|
||||||
}
|
}
|
||||||
(false, false, false, t)
|
(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 {
|
if !first_of_pic {
|
||||||
group_start = None;
|
group_start = None;
|
||||||
@@ -736,7 +821,11 @@ fn rebuild_with(src: &dyn Source, off: u64, avail: u64, preset: Option<&MediaCon
|
|||||||
None => (30, 1),
|
None => (30, 1),
|
||||||
};
|
};
|
||||||
let fps = num as f64 / den as f64;
|
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;
|
let fps = num as f64 / den as f64;
|
||||||
// Escala: tique do quadro = den, escala de tempo = num.
|
// Escala: tique do quadro = den, escala de tempo = num.
|
||||||
let (timescale, dur) = (num, den);
|
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());
|
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 payload = (data_start, payload_end);
|
||||||
let out_payload_start = pre_len + 16;
|
let out_payload_start = pre_len + 16;
|
||||||
let map_off = |abs: u64| -> u64 { out_payload_start + (abs - data_start) };
|
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();
|
let used: u64 = samples.iter().map(|s| s.size as u64).sum();
|
||||||
Some(Rebuilt {
|
Some(Rebuilt {
|
||||||
pre_len,
|
pre_len,
|
||||||
@@ -846,7 +953,14 @@ fn build_moov(
|
|||||||
let (fourcc, cfg) = match codec {
|
let (fourcc, cfg) = match codec {
|
||||||
Codec::Avc => {
|
Codec::Avc => {
|
||||||
let s = avc.as_ref().unwrap();
|
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 {
|
for x in sps {
|
||||||
c.extend_from_slice(&(x.len() as u16).to_be_bytes());
|
c.extend_from_slice(&(x.len() as u16).to_be_bytes());
|
||||||
c.extend_from_slice(x);
|
c.extend_from_slice(x);
|
||||||
@@ -893,14 +1007,24 @@ fn build_moov(
|
|||||||
stsd_body.extend_from_slice(&sample_entry);
|
stsd_body.extend_from_slice(&sample_entry);
|
||||||
let stsd = full(b"stsd", 0, 0, &stsd_body);
|
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();
|
let mut stsz_body = u32b(0).to_vec();
|
||||||
stsz_body.extend_from_slice(&u32b(n as u32));
|
stsz_body.extend_from_slice(&u32b(n as u32));
|
||||||
for s in samples {
|
for s in samples {
|
||||||
stsz_body.extend_from_slice(&u32b(s.size));
|
stsz_body.extend_from_slice(&u32b(s.size));
|
||||||
}
|
}
|
||||||
let stsz = full(b"stsz", 0, 0, &stsz_body);
|
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 need64 = samples.iter().any(|s| map_off(s.off) > u32::MAX as u64);
|
||||||
let stco = if need64 {
|
let stco = if need64 {
|
||||||
let mut b = u32b(n as u32).to_vec();
|
let mut b = u32b(n as u32).to_vec();
|
||||||
@@ -915,7 +1039,12 @@ fn build_moov(
|
|||||||
}
|
}
|
||||||
full(b"stco", 0, 0, &b)
|
full(b"stco", 0, 0, &b)
|
||||||
};
|
};
|
||||||
let syncs: Vec<u32> = samples.iter().enumerate().filter(|(_, s)| s.sync).map(|(i, _)| i as u32 + 1).collect();
|
let syncs: Vec<u32> = samples
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, s)| s.sync)
|
||||||
|
.map(|(i, _)| i as u32 + 1)
|
||||||
|
.collect();
|
||||||
let mut stbl_parts = vec![stsd, stts];
|
let mut stbl_parts = vec![stsd, stts];
|
||||||
if let Some(c) = ctts {
|
if let Some(c) = ctts {
|
||||||
// entradas agrupadas
|
// 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.
|
/// 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 {
|
match codec {
|
||||||
Codec::Avc => {
|
Codec::Avc => {
|
||||||
let t = head[0] & 0x1F;
|
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 => {
|
1 | 2 | 5 => {
|
||||||
let rbsp = unescape(&head[1..]);
|
let rbsp = unescape(&head[1..]);
|
||||||
let mut b = Bits::new(&rbsp);
|
let mut b = Bits::new(&rbsp);
|
||||||
let (Some(first), Some(st), Some(pps)) = (b.ue(), b.ue(), b.ue()) else { return false };
|
let (Some(first), Some(st), Some(pps)) = (b.ue(), b.ue(), b.ue()) else {
|
||||||
if st > 9 || pps > 255 || (pps_count > 0 && pps as usize >= pps_count.max(1) && pps > 3) {
|
return false;
|
||||||
|
};
|
||||||
|
if st > 9
|
||||||
|
|| pps > 255
|
||||||
|
|| (pps_count > 0 && pps as usize >= pps_count.max(1) && pps > 3)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if t == 5 && !matches!(st, 2 | 4 | 7 | 9) {
|
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;
|
let t = (head[0] >> 1) & 0x3F;
|
||||||
match t {
|
match t {
|
||||||
35 => len <= 4,
|
35 => len <= 4,
|
||||||
32 | 33 | 34 => (4..=1000).contains(&len),
|
32..=34 => (4..=1000).contains(&len),
|
||||||
_ => true,
|
_ => 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`).
|
/// 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 p = (at - base) as usize;
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
while n < max && p + 8 <= buf.len() {
|
while n < max && p + 8 <= buf.len() {
|
||||||
let l = be32(buf, p) as u64;
|
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 {
|
&& match codec {
|
||||||
Codec::Avc => avc_nal_ok(buf[p + 4]),
|
Codec::Avc => avc_nal_ok(buf[p + 4]),
|
||||||
Codec::Hevc => hevc_nal_ok(buf[p + 4], buf[p + 5]),
|
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 {
|
if !ok {
|
||||||
break;
|
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.
|
/// 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.
|
/// 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<u64> {
|
fn scan_nal(
|
||||||
|
src: &dyn Source,
|
||||||
|
codec: Codec,
|
||||||
|
from: u64,
|
||||||
|
limit: u64,
|
||||||
|
data_end: u64,
|
||||||
|
sps: Option<&Sps264>,
|
||||||
|
pps_count: usize,
|
||||||
|
cap: u64,
|
||||||
|
) -> Option<u64> {
|
||||||
if limit <= from + 8 {
|
if limit <= from + 8 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// Lê um pouco além do limite para poder seguir a cadeia de unidades.
|
// 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 buf = read_vec(src, from, want).ok()?;
|
||||||
let mut single = None;
|
let mut single = None;
|
||||||
let mut i = 0usize;
|
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 nb = buf[i + 4];
|
||||||
let good_head = match codec {
|
let good_head = match codec {
|
||||||
// Um quadro novo começa por parâmetros/AUD/SEI ou por fatia do macrobloco 0.
|
// 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]),
|
Codec::Hevc => hevc_nal_ok(nb, buf[i + 5]),
|
||||||
};
|
};
|
||||||
if good_head {
|
if good_head {
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ pub fn detect_fs(src: &dyn Source, start: u64) -> Option<(FsKind, Option<u64>)>
|
|||||||
// Assinaturas mais adiante no volume: Btrfs (64 KiB) e RAID mdadm 1.2 (4 KiB).
|
// 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 let Ok(h) = read_vec(src, start + 0x10000, 0x100) {
|
||||||
if h.len() >= 0x80 && &h[0x40..0x48] == b"_BHRfS_M" {
|
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) {
|
if let Ok(h) = read_vec(src, start + 4096, 16) {
|
||||||
@@ -68,7 +71,10 @@ pub fn detect_fs_in(b: &[u8]) -> Option<(FsKind, Option<u64>)> {
|
|||||||
return Some((FsKind::Refs, Some(sectors * bps)));
|
return Some((FsKind::Refs, Some(sectors * bps)));
|
||||||
}
|
}
|
||||||
if crate::crypt::filevault::detect(b) {
|
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) {
|
if crate::crypt::bitlocker::detect(b) {
|
||||||
return Some((FsKind::Bitlocker, None));
|
return Some((FsKind::Bitlocker, None));
|
||||||
|
|||||||
@@ -60,7 +60,12 @@ impl Layout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn all_raid5() -> [Layout; 4] {
|
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 {
|
impl RaidParams {
|
||||||
pub fn simple(level: Level, chunk: u64, layout: Layout, members: Vec<Option<Member>>) -> Self {
|
pub fn simple(level: Level, chunk: u64, layout: Layout, members: Vec<Option<Member>>) -> 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 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 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 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 {
|
if p.level != Level::Linear {
|
||||||
per_member = per_member / p.chunk.max(512) * p.chunk.max(512);
|
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),
|
Level::R10 => (p.near.max(1) as usize).saturating_sub(1).min(1),
|
||||||
};
|
};
|
||||||
if missing > need && p.level != Level::R10 {
|
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 {
|
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());
|
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());
|
return Err("parâmetros de RAID 10 inválidos".into());
|
||||||
}
|
}
|
||||||
let size = match p.level {
|
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::R0 => per_member * n as u64,
|
||||||
Level::R1 => per_member,
|
Level::R1 => per_member,
|
||||||
Level::R4 | Level::R5 => per_member * (n as u64 - 1),
|
Level::R4 | Level::R5 => per_member * (n as u64 - 1),
|
||||||
Level::R6 => per_member * (n as u64 - 2),
|
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,
|
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 {
|
pub fn params(&self) -> &RaidParams {
|
||||||
@@ -186,10 +214,14 @@ impl RaidSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_member(&self, i: usize, off: u64, buf: &mut [u8]) -> io::Result<()> {
|
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;
|
let mut done = 0;
|
||||||
while done < buf.len() {
|
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 {
|
if n == 0 {
|
||||||
buf[done..].fill(0);
|
buf[done..].fill(0);
|
||||||
break;
|
break;
|
||||||
@@ -206,12 +238,24 @@ impl RaidSource {
|
|||||||
match self.p.layout {
|
match self.p.layout {
|
||||||
Layout::LeftAsymmetric | Layout::LeftSymmetric => {
|
Layout::LeftAsymmetric | Layout::LeftSymmetric => {
|
||||||
let pd = n - 1 - r;
|
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)
|
(pd, disk)
|
||||||
}
|
}
|
||||||
Layout::RightAsymmetric | Layout::RightSymmetric => {
|
Layout::RightAsymmetric | Layout::RightSymmetric => {
|
||||||
let pd = r;
|
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)
|
(pd, disk)
|
||||||
}
|
}
|
||||||
Layout::ParityFirst => (0, d + 1),
|
Layout::ParityFirst => (0, d + 1),
|
||||||
@@ -273,11 +317,13 @@ impl RaidSource {
|
|||||||
}
|
}
|
||||||
Level::R1 => {
|
Level::R1 => {
|
||||||
for i in 0..n {
|
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(());
|
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 => {
|
Level::R4 | Level::R5 => {
|
||||||
let dd = n - 1;
|
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 {
|
if self.p.far_offset {
|
||||||
stripe *= far;
|
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 out = vec![];
|
||||||
let mut d = dev;
|
let mut d = dev;
|
||||||
let mut off = stripe * c;
|
let mut off = stripe * c;
|
||||||
@@ -367,7 +418,9 @@ impl RaidSource {
|
|||||||
let len = buf.len();
|
let len = buf.len();
|
||||||
// Estado de cada dado da linha.
|
// Estado de cada dado da linha.
|
||||||
let disks: Vec<usize> = (0..dd).map(|k| self.raid6_map(row, k).2).collect();
|
let disks: Vec<usize> = (0..dd).map(|k| self.raid6_map(row, k).2).collect();
|
||||||
let missing_data: Vec<usize> = (0..dd).filter(|&k| self.p.members[disks[k]].is_none()).collect();
|
let missing_data: Vec<usize> = (0..dd)
|
||||||
|
.filter(|&k| self.p.members[disks[k]].is_none())
|
||||||
|
.collect();
|
||||||
let p_ok = self.p.members[pd].is_some();
|
let p_ok = self.p.members[pd].is_some();
|
||||||
let q_ok = self.p.members[qd].is_some();
|
let q_ok = self.p.members[qd].is_some();
|
||||||
let mut data: Vec<Option<Vec<u8>>> = vec![None; dd];
|
let mut data: Vec<Option<Vec<u8>>> = vec![None; dd];
|
||||||
@@ -437,7 +490,9 @@ impl RaidSource {
|
|||||||
}
|
}
|
||||||
return Ok(());
|
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 {
|
if offset + done as u64 >= base && offset + (done as u64) < base + len {
|
||||||
let rel = offset + done as u64 - base;
|
let rel = offset + done as u64 - base;
|
||||||
let n = ((len - rel) as usize).min(want - done);
|
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 {
|
if got == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -541,7 +598,9 @@ pub fn read_md_super(src: &dyn Source) -> Option<MdSuper> {
|
|||||||
cands.push(((size & !4095) - 8192, "1.0"));
|
cands.push(((size & !4095) - 8192, "1.0"));
|
||||||
}
|
}
|
||||||
for (off, ver) in cands {
|
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 {
|
if b.len() < 4096 || le32(&b, 0) != 0xA92B_4EFC || le32(&b, 4) != 1 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -556,7 +615,9 @@ pub fn read_md_super(src: &dyn Source) -> Option<MdSuper> {
|
|||||||
};
|
};
|
||||||
let mut uuid = [0u8; 16];
|
let mut uuid = [0u8; 16];
|
||||||
uuid.copy_from_slice(&b[16..32]);
|
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_offset = le64(&b, 128) * 512;
|
||||||
let data_size = le64(&b, 136) * 512;
|
let data_size = le64(&b, 136) * 512;
|
||||||
let raid_disks = le32(&b, 92);
|
let raid_disks = le32(&b, 92);
|
||||||
@@ -661,12 +722,20 @@ pub fn assemble_md(members: Vec<(Arc<dyn Source>, MdSuper)>) -> Vec<Result<Found
|
|||||||
stale += 1;
|
stale += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
slots[r as usize] = Some(Member { src: s.clone(), data_offset: m.data_offset });
|
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 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);
|
let mut p = RaidParams::simple(
|
||||||
|
top.level,
|
||||||
|
top.chunk.max(512),
|
||||||
|
Layout::from_md(top.layout),
|
||||||
|
slots,
|
||||||
|
);
|
||||||
if top.level == Level::R10 {
|
if top.level == Level::R10 {
|
||||||
p.near = top.layout & 0xff;
|
p.near = top.layout & 0xff;
|
||||||
p.far = (top.layout >> 8) & 0xff;
|
p.far = (top.layout >> 8) & 0xff;
|
||||||
|
|||||||
@@ -23,13 +23,23 @@ impl Candidate {
|
|||||||
self.level,
|
self.level,
|
||||||
self.chunk,
|
self.chunk,
|
||||||
self.layout,
|
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 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<Vec<usize>> {
|
fn permutations(n: usize) -> Vec<Vec<usize>> {
|
||||||
fn rec(cur: &mut Vec<usize>, used: &mut Vec<bool>, n: usize, out: &mut Vec<Vec<usize>>) {
|
fn rec(cur: &mut Vec<usize>, used: &mut Vec<bool>, n: usize, out: &mut Vec<Vec<usize>>) {
|
||||||
@@ -53,7 +63,9 @@ fn permutations(n: usize) -> Vec<Vec<usize>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ntfs_score(src: &dyn Source, start: u64) -> f64 {
|
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 " {
|
if b.len() < 512 || &b[3..11] != b"NTFS " {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
@@ -61,18 +73,26 @@ fn ntfs_score(src: &dyn Source, start: u64) -> f64 {
|
|||||||
let spc = b[13] as u64;
|
let spc = b[13] as u64;
|
||||||
let mft = u64::from_le_bytes(b[0x30..0x38].try_into().unwrap());
|
let mft = u64::from_le_bytes(b[0x30..0x38].try_into().unwrap());
|
||||||
let rs = b[0x40] as i8;
|
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 {
|
if bps == 0 || spc == 0 || rec == 0 || rec > 65536 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
let mft_off = start + mft * bps * spc;
|
let mft_off = start + mft * bps * spc;
|
||||||
let (mut ok, mut total) = (0, 0);
|
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;
|
let off = mft_off + k * rec;
|
||||||
if off + rec > src.size() {
|
if off + rec > src.size() {
|
||||||
break;
|
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 {
|
if r.len() < 64 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -106,18 +126,25 @@ pub fn fs_score(src: &dyn Source) -> f64 {
|
|||||||
let layout = partition::analyze(src, false);
|
let layout = partition::analyze(src, false);
|
||||||
let mut best = 0.0f64;
|
let mut best = 0.0f64;
|
||||||
for p in &layout.partitions {
|
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
|
best
|
||||||
}
|
}
|
||||||
|
|
||||||
fn first_sector_plausible(src: &dyn Source) -> bool {
|
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())
|
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).
|
/// Testa as combinações e devolve as melhores (maior pontuação primeiro).
|
||||||
pub fn detect(members: &[Arc<dyn Source>], cancelled: &(dyn Fn() -> bool + Sync)) -> Vec<Candidate> {
|
pub fn detect(
|
||||||
|
members: &[Arc<dyn Source>],
|
||||||
|
cancelled: &(dyn Fn() -> bool + Sync),
|
||||||
|
) -> Vec<Candidate> {
|
||||||
let n = members.len();
|
let n = members.len();
|
||||||
if n < 2 {
|
if n < 2 {
|
||||||
return vec![];
|
return vec![];
|
||||||
@@ -140,13 +167,22 @@ pub fn detect(members: &[Arc<dyn Source>], cancelled: &(dyn Fn() -> bool + Sync)
|
|||||||
let probe = |s: &Arc<dyn Source>| read_vec(s.as_ref(), 0, 1 << 20).unwrap_or_default();
|
let probe = |s: &Arc<dyn Source>| read_vec(s.as_ref(), 0, 1 << 20).unwrap_or_default();
|
||||||
let first = probe(&cached[0]);
|
let first = probe(&cached[0]);
|
||||||
if first.iter().any(|&x| x != 0) && cached.iter().skip(1).all(|c| probe(c) == first) {
|
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);
|
out.push(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RAID 5/4 satisfaz "XOR de todos os discos = 0" em toda faixa.
|
// RAID 5/4 satisfaz "XOR de todos os discos = 0" em toda faixa.
|
||||||
let sample = 4usize << 20;
|
let sample = 4usize << 20;
|
||||||
let bufs: Vec<Vec<u8>> = cached.iter().map(|c| read_vec(c.as_ref(), 0, sample).unwrap_or_default()).collect();
|
let bufs: Vec<Vec<u8>> = cached
|
||||||
|
.iter()
|
||||||
|
.map(|c| read_vec(c.as_ref(), 0, sample).unwrap_or_default())
|
||||||
|
.collect();
|
||||||
let mut xor_zero = 0usize;
|
let mut xor_zero = 0usize;
|
||||||
let mut nonzero_rows = 0usize;
|
let mut nonzero_rows = 0usize;
|
||||||
for blk in 0..sample / 4096 {
|
for blk in 0..sample / 4096 {
|
||||||
@@ -181,20 +217,35 @@ pub fn detect(members: &[Arc<dyn Source>], cancelled: &(dyn Fn() -> bool + Sync)
|
|||||||
levels.push(Level::R6);
|
levels.push(Level::R6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let ok_first: Vec<bool> = cached.iter().map(|c| first_sector_plausible(c.as_ref())).collect();
|
let ok_first: Vec<bool> = cached
|
||||||
|
.iter()
|
||||||
|
.map(|c| first_sector_plausible(c.as_ref()))
|
||||||
|
.collect();
|
||||||
let any_first = ok_first.iter().any(|&b| b);
|
let any_first = ok_first.iter().any(|&b| b);
|
||||||
let perms = permutations(n);
|
let perms = permutations(n);
|
||||||
for level in levels {
|
for level in levels {
|
||||||
let layouts: Vec<Layout> = if matches!(level, Level::R0) { vec![Layout::LeftSymmetric] } else { Layout::all_raid5().to_vec() };
|
let layouts: Vec<Layout> = if matches!(level, Level::R0) {
|
||||||
|
vec![Layout::LeftSymmetric]
|
||||||
|
} else {
|
||||||
|
Layout::all_raid5().to_vec()
|
||||||
|
};
|
||||||
for &chunk in &CHUNKS {
|
for &chunk in &CHUNKS {
|
||||||
for layout in &layouts {
|
for layout in &layouts {
|
||||||
for order in &perms {
|
for order in &perms {
|
||||||
if cancelled() {
|
if cancelled() {
|
||||||
return finish(out);
|
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 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.
|
// O primeiro setor do volume precisa estar num disco que o tenha.
|
||||||
if any_first {
|
if any_first {
|
||||||
let mut b = [0u8; 512];
|
let mut b = [0u8; 512];
|
||||||
@@ -221,7 +272,11 @@ pub fn detect(members: &[Arc<dyn Source>], cancelled: &(dyn Fn() -> bool + Sync)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn finish(mut out: Vec<Candidate>) -> Vec<Candidate> {
|
fn finish(mut out: Vec<Candidate>) -> Vec<Candidate> {
|
||||||
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.truncate(10);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -274,8 +274,12 @@ impl Handler<'_> {
|
|||||||
refs.push(c);
|
refs.push(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let Some(r) = crate::mp4rebuild::rebuild(self.src, off, p.hit_len, &refs) else { return };
|
let Some(r) = crate::mp4rebuild::rebuild(self.src, off, p.hit_len, &refs) else {
|
||||||
let Ok(pre) = crate::source::read_vec(self.src, off, r.pre_len as usize) else { return };
|
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 payload_len = r.payload.1 - r.payload.0;
|
||||||
let mut f = p.found;
|
let mut f = p.found;
|
||||||
let hdr = r.mdat_header();
|
let hdr = r.mdat_header();
|
||||||
@@ -284,7 +288,10 @@ impl Handler<'_> {
|
|||||||
parts: vec![
|
parts: vec![
|
||||||
Part::Bytes { bytes: pre },
|
Part::Bytes { bytes: pre },
|
||||||
Part::Bytes { bytes: hdr },
|
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 },
|
Part::Bytes { bytes: r.moov },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -311,13 +318,23 @@ impl Handler<'_> {
|
|||||||
used.extend(self.carved.iter().copied());
|
used.extend(self.carved.iter().copied());
|
||||||
// O próprio começo do arquivo não conta como "ocupado" para ele mesmo.
|
// O próprio começo do arquivo não conta como "ocupado" para ele mesmo.
|
||||||
used.retain(|e| !(e.offset == off));
|
used.retain(|e| !(e.offset == off));
|
||||||
let pool = crate::defrag::Pool { index: &self.index, used: &used };
|
let pool = crate::defrag::Pool {
|
||||||
if let crate::defrag::Analysis::Rebuilt { extents, size } = crate::defrag::analyze_jpeg(self.src, off, p.hit_len, &pool, true) {
|
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;
|
let mut f = p.found;
|
||||||
f.size = size;
|
f.size = size;
|
||||||
f.data = DataLoc::Extents { extents: extents.clone() };
|
f.data = DataLoc::Extents {
|
||||||
|
extents: extents.clone(),
|
||||||
|
};
|
||||||
f.confidence = Confidence::Medium;
|
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.carved.extend(extents);
|
||||||
self.sink.replace(f);
|
self.sink.replace(f);
|
||||||
}
|
}
|
||||||
@@ -354,7 +371,10 @@ impl CarveEvents for Handler<'_> {
|
|||||||
let mut skip_len = None;
|
let mut skip_len = None;
|
||||||
let mut pending_len = None;
|
let mut pending_len = None;
|
||||||
if hit.ext == "jpg" {
|
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) {
|
match crate::defrag::analyze_jpeg(self.src, off, hit.len, &pool, false) {
|
||||||
crate::defrag::Analysis::Intact { len } => {
|
crate::defrag::Analysis::Intact { len } => {
|
||||||
hit.len = len;
|
hit.len = len;
|
||||||
@@ -412,8 +432,16 @@ impl CarveEvents for Handler<'_> {
|
|||||||
self.carved.push(Extent::new(off, hit.len));
|
self.carved.push(Extent::new(off, hit.len));
|
||||||
if let Some(hl) = pending_len {
|
if let Some(hl) = pending_len {
|
||||||
let mut pf = f.clone();
|
let mut pf = f.clone();
|
||||||
pf.note = Some(if pending_mp4 { "vídeo sem índice (moov)".into() } else { "JPEG interrompido ou fragmentado".into() });
|
pf.note = Some(if pending_mp4 {
|
||||||
self.pending.push(Pending { found: pf, hit_len: hl, mp4: 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);
|
self.sink.found(f);
|
||||||
match skip_len {
|
match skip_len {
|
||||||
|
|||||||
@@ -225,14 +225,25 @@ impl Engine {
|
|||||||
if opts.assembly.remember_keys {
|
if opts.assembly.remember_keys {
|
||||||
opts.assembly.keys = asm.keys.clone();
|
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();
|
let mut s = job.st.lock().unwrap();
|
||||||
for u in &asm.unlocked {
|
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 {
|
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());
|
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).
|
/// 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<String> {
|
pub fn start_image_as(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
dest: &str,
|
||||||
|
format: &image::ImageFormat,
|
||||||
|
) -> Result<String> {
|
||||||
let format = format.clone();
|
let format = format.clone();
|
||||||
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
||||||
recover::check_destination(
|
recover::check_destination(
|
||||||
@@ -848,8 +864,14 @@ impl Engine {
|
|||||||
let mut last_err = None;
|
let mut last_err = None;
|
||||||
for path in &tried {
|
for path in &tried {
|
||||||
match open_path(Path::new(path)) {
|
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()) => {
|
Ok(src)
|
||||||
*j.src.lock().unwrap() = Some(finish_source(&j, 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();
|
*j.src_path.lock().unwrap() = path.clone();
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -860,7 +882,7 @@ impl Engine {
|
|||||||
if let Some((path, src)) =
|
if let Some((path, src)) =
|
||||||
crate::reconnect::find_matching_source(size, fp.as_deref(), &tried)
|
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();
|
*j.src_path.lock().unwrap() = path.clone();
|
||||||
return Ok(Some(path));
|
return Ok(Some(path));
|
||||||
}
|
}
|
||||||
@@ -1019,10 +1041,15 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
layout.warnings.extend(a.notes.iter().cloned());
|
layout.warnings.extend(a.notes.iter().cloned());
|
||||||
for u in &a.unlocked {
|
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 {
|
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)
|
json!(layout)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,19 @@ pub fn category_for_ext(ext: &str) -> Category {
|
|||||||
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp" | "heic" | "avif" | "tif" | "tiff"
|
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp" | "heic" | "avif" | "tif" | "tiff"
|
||||||
| "psd" | "svg" | "ico" | "cr2" | "nef" | "dng" | "jp2" => Category::Image,
|
| "psd" | "svg" | "ico" | "cr2" | "nef" | "dng" | "jp2" => Category::Image,
|
||||||
"pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" | "ods" | "odp"
|
"pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" | "ods" | "odp"
|
||||||
| "txt" | "rtf" | "epub" | "md" | "csv" | "html" | "xml" | "json" | "swf" => Category::Document,
|
| "txt" | "rtf" | "epub" | "md" | "csv" | "html" | "xml" | "json" | "swf" => {
|
||||||
"mp4" | "mov" | "m4v" | "avi" | "mkv" | "webm" | "wmv" | "flv" | "mpg" | "mpeg" | "3gp" | "ts" => {
|
Category::Document
|
||||||
Category::Video
|
}
|
||||||
|
"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,
|
_ => Category::Other,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,22 +91,22 @@ pub fn matches_ext(ext: &str, head: &[u8]) -> Option<bool> {
|
|||||||
pub fn might_start(b0: u8) -> bool {
|
pub fn might_start(b0: u8) -> bool {
|
||||||
crate::formats::might_start(b0)
|
crate::formats::might_start(b0)
|
||||||
|| matches!(
|
|| matches!(
|
||||||
b0,
|
b0,
|
||||||
0xFF | 0x89
|
0xFF | 0x89
|
||||||
| b'G'
|
| b'G'
|
||||||
| b'B'
|
| b'B'
|
||||||
| b'%'
|
| b'%'
|
||||||
| b'P'
|
| b'P'
|
||||||
| 0xD0
|
| 0xD0
|
||||||
| b'7'
|
| b'7'
|
||||||
| b'S'
|
| b'S'
|
||||||
| b'R'
|
| b'R'
|
||||||
| b'I'
|
| b'I'
|
||||||
| b'M'
|
| b'M'
|
||||||
| 0x00
|
| 0x00
|
||||||
| b'f'
|
| b'f'
|
||||||
| 0x1F
|
| 0x1F
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_HEADER: usize = 64 * 1024;
|
const MAX_HEADER: usize = 64 * 1024;
|
||||||
|
|||||||
@@ -277,11 +277,13 @@ pub fn open_path(path: &Path) -> io::Result<Arc<dyn Source>> {
|
|||||||
let mut head = [0u8; 96];
|
let mut head = [0u8; 96];
|
||||||
let got = file.read_at(0, &mut head).unwrap_or(0);
|
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]) {
|
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));
|
return Ok(Arc::new(a));
|
||||||
}
|
}
|
||||||
if got >= 8 && crate::ewf::is_ewf_header(&head) {
|
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));
|
return Ok(Arc::new(e));
|
||||||
}
|
}
|
||||||
Ok(Arc::new(file))
|
Ok(Arc::new(file))
|
||||||
|
|||||||
@@ -154,19 +154,30 @@ impl Packed {
|
|||||||
pub fn read(&self, src: &dyn crate::source::Source) -> std::io::Result<Vec<u8>> {
|
pub fn read(&self, src: &dyn crate::source::Source) -> std::io::Result<Vec<u8>> {
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
let raw = crate::source::read_vec(src, self.offset, self.disk_len as usize)?;
|
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 {
|
let mut data = match self.codec {
|
||||||
PackedCodec::Zlib => {
|
PackedCodec::Zlib => {
|
||||||
let mut v = Vec::with_capacity(self.ram_len as usize);
|
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
|
v
|
||||||
}
|
}
|
||||||
PackedCodec::Zstd => {
|
PackedCodec::Zstd => {
|
||||||
let mut v = Vec::with_capacity(self.ram_len as usize);
|
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
|
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);
|
data.resize(self.ram_len.max(data.len() as u64) as usize, 0);
|
||||||
let s = (self.skip as usize).min(data.len());
|
let s = (self.skip as usize).min(data.len());
|
||||||
@@ -193,14 +204,25 @@ impl Part {
|
|||||||
mod b64 {
|
mod b64 {
|
||||||
use serde::{Deserialize, Deserializer, Serializer};
|
use serde::{Deserialize, Deserializer, Serializer};
|
||||||
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
#[allow(clippy::ptr_arg)]
|
||||||
pub fn serialize<S: Serializer>(b: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
|
pub fn serialize<S: Serializer>(b: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
|
||||||
let mut out = String::with_capacity(b.len().div_ceil(3) * 4);
|
let mut out = String::with_capacity(b.len().div_ceil(3) * 4);
|
||||||
for c in b.chunks(3) {
|
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 >> 18) as usize & 63] as char);
|
||||||
out.push(T[(n >> 12) 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() > 1 {
|
||||||
out.push(if c.len() > 2 { T[n as usize & 63] as char } else { '=' });
|
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)
|
s.serialize_str(&out)
|
||||||
}
|
}
|
||||||
@@ -386,7 +408,16 @@ impl FsKind {
|
|||||||
pub fn has_metadata_scanner(self) -> bool {
|
pub fn has_metadata_scanner(self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,17 @@ pub struct Overlay {
|
|||||||
impl Overlay {
|
impl Overlay {
|
||||||
pub fn new(base: Arc<dyn Source>, mut regions: Vec<Region>) -> Self {
|
pub fn new(base: Arc<dyn Source>, mut regions: Vec<Region>) -> Self {
|
||||||
regions.sort_by_key(|r| r.start);
|
regions.sort_by_key(|r| r.start);
|
||||||
let size = regions.iter().map(|r| r.start + r.len).max().unwrap_or(0).max(base.size());
|
let size = regions
|
||||||
Self { base, regions, size }
|
.iter()
|
||||||
|
.map(|r| r.start + r.len)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0)
|
||||||
|
.max(base.size());
|
||||||
|
Self {
|
||||||
|
base,
|
||||||
|
regions,
|
||||||
|
size,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn regions(&self) -> &[Region] {
|
pub fn regions(&self) -> &[Region] {
|
||||||
&self.regions
|
&self.regions
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ fn dir() -> PathBuf {
|
|||||||
#[test]
|
#[test]
|
||||||
fn reads_reference_images_and_stored_hash_matches() {
|
fn reads_reference_images_and_stored_hash_matches() {
|
||||||
let mut seen = 0;
|
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);
|
let p = dir().join(name);
|
||||||
if !p.exists() {
|
if !p.exists() {
|
||||||
eprintln!("SKIP {name}");
|
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}"));
|
let a = Aff4Source::open(&p).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||||
assert_eq!(a.size(), 268_435_456, "{name}");
|
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
|
// setor 0 legível pelo caminho genérico
|
||||||
let s = open_path(&p).unwrap();
|
let s = open_path(&p).unwrap();
|
||||||
let head = read_vec(s.as_ref(), 0, 4096).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
|
// leitura em vãos do mapa devolve zeros
|
||||||
let far = read_vec(s.as_ref(), 200 << 20, 512).unwrap();
|
let far = read_vec(s.as_ref(), 200 << 20, 512).unwrap();
|
||||||
assert_eq!(far.len(), 512);
|
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;
|
seen += 1;
|
||||||
}
|
}
|
||||||
if seen == 0 {
|
if seen == 0 {
|
||||||
@@ -43,17 +56,36 @@ fn roundtrip_with_snappy_blocks_and_odd_size() {
|
|||||||
x ^= x << 13;
|
x ^= x << 13;
|
||||||
x ^= x >> 7;
|
x ^= x >> 7;
|
||||||
x ^= x << 17;
|
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();
|
.collect();
|
||||||
data.truncate(5_000_001);
|
data.truncate(5_000_001);
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let p = dir.path().join("t.aff4");
|
let p = dir.path().join("t.aff4");
|
||||||
let opts = Aff4Options { case_name: "caso".into(), examiner: "Lucas".into(), notes: "teste".into(), ..Default::default() };
|
let opts = Aff4Options {
|
||||||
let rep = write_aff4(&MemSource(data.clone()), &p, &opts, &|| false, &mut |_, _| {}).unwrap();
|
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();
|
let a = Aff4Source::open(&p).unwrap();
|
||||||
assert_eq!(a.size(), data.len() as u64);
|
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.info.get("Examinador").map(String::as_str), Some("Lucas"));
|
||||||
assert_eq!(a.verify_stream_md5().unwrap(), Some(true));
|
assert_eq!(a.verify_stream_md5().unwrap(), Some(true));
|
||||||
let got = read_vec(&a, 0, data.len()).unwrap();
|
let got = read_vec(&a, 0, data.len()).unwrap();
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ fn reads_the_live_tree_of_the_reference_image() {
|
|||||||
eprintln!("{files:#?}");
|
eprintln!("{files:#?}");
|
||||||
assert!(!files.is_empty());
|
assert!(!files.is_empty());
|
||||||
let names: Vec<&str> = files.iter().map(|f| f.0.as_str()).collect();
|
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]
|
#[test]
|
||||||
@@ -28,16 +31,31 @@ fn checksum_matches_real_blocks() {
|
|||||||
let Some(p) = img() else { return };
|
let Some(p) = img() else { return };
|
||||||
let d = std::fs::read(p).unwrap();
|
let d = std::fs::read(p).unwrap();
|
||||||
let b = &d[0..4096];
|
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]
|
#[test]
|
||||||
fn quick_scan_is_clean_on_pristine_volume() {
|
fn quick_scan_is_clean_on_pristine_volume() {
|
||||||
let Some(p) = img() else { return };
|
let Some(p) = img() else { return };
|
||||||
init();
|
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);
|
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::<Vec<_>>());
|
eprintln!(
|
||||||
|
"{:?}",
|
||||||
|
c.found
|
||||||
|
.iter()
|
||||||
|
.map(|f| (&f.name, &f.dir, f.size))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- manipulação bruta de nós (independente do módulo) ---
|
// --- manipulação bruta de nós (independente do módulo) ---
|
||||||
@@ -65,7 +83,10 @@ fn read_node(b: &[u8]) -> (u16, Vec<Entry>) {
|
|||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
let o = 56 + toc_off + i * 8;
|
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));
|
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)
|
(flags, out)
|
||||||
}
|
}
|
||||||
@@ -131,7 +152,12 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
|
|||||||
c
|
c
|
||||||
});
|
});
|
||||||
let vol_oid = le64(&nx, 184);
|
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 vsb = blk(&d, vol_paddr);
|
||||||
let (vol_omap_paddr, root_oid) = (le64(&vsb, 128), le64(&vsb, 136));
|
let (vol_omap_paddr, root_oid) = (le64(&vsb, 128), le64(&vsb, 136));
|
||||||
let vol_omap = blk(&d, vol_omap_paddr);
|
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();
|
let vt_orig = vt.clone();
|
||||||
fixed_to_var(&mut vt);
|
fixed_to_var(&mut vt);
|
||||||
let (_, omap_entries) = read_node(&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);
|
let root = blk(&d, root_paddr);
|
||||||
assert_eq!(le16(&root, 34), 0, "raiz deveria ser folha nesta imagem");
|
assert_eq!(le16(&root, 34), 0, "raiz deveria ser folha nesta imagem");
|
||||||
let (_, recs) = read_node(&root);
|
let (_, recs) = read_node(&root);
|
||||||
// inode do arquivo
|
// inode do arquivo
|
||||||
let victim = b"passwords.txt";
|
let victim = b"passwords.txt";
|
||||||
let ino = recs.iter().find_map(|(k, v)| {
|
let ino = recs
|
||||||
let hdr = le64(k, 0);
|
.iter()
|
||||||
((hdr >> 60) == 9 && k.len() > 12 && k[12..].starts_with(victim)).then(|| le64(v, 0))
|
.find_map(|(k, v)| {
|
||||||
}).expect("entrada de diretório");
|
let hdr = le64(k, 0);
|
||||||
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();
|
((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<Entry> = recs
|
let kept: Vec<Entry> = recs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(k, v)| {
|
.filter(|(k, v)| {
|
||||||
let hdr = le64(k, 0);
|
let hdr = le64(k, 0);
|
||||||
let (id, ty) = (hdr & 0x0FFF_FFFF_FFFF_FFFF, hdr >> 60);
|
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)
|
!(id == ino && ty != 9 || id == private_id && ty == 8 || drec_of_victim)
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -164,7 +206,9 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
|
|||||||
assert!(kept.len() < recs.len());
|
assert!(kept.len() < recs.len());
|
||||||
let newnode = write_node(&root, &kept);
|
let newnode = write_node(&root, &kept);
|
||||||
// bloco livre para a nova versão
|
// 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);
|
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
|
// aponta o mapa de objetos para a nova versão (mesmo xid), editando o valor da entrada
|
||||||
let toc_off = le16(&vt_orig, 40);
|
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();
|
let src = FileSource::open(&p2).unwrap();
|
||||||
assert!(!live_paths(&src, 0).iter().any(|f| f.0 == "passwords.txt"));
|
assert!(!live_paths(&src, 0).iter().any(|f| f.0 == "passwords.txt"));
|
||||||
init();
|
init();
|
||||||
let c = scan_path(&p2, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
|
let c = scan_path(
|
||||||
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::<Vec<_>>()));
|
&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::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
assert_eq!(f.size, 116);
|
assert_eq!(f.size, 116);
|
||||||
let out = dir.path().join("out");
|
let out = dir.path().join("out");
|
||||||
let rep = recover_all(&p2, std::slice::from_ref(f), &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 rp = PathBuf::from(&rep.recovered[0].path);
|
||||||
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
|
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
|
||||||
assert_eq!(got.len(), 116);
|
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.
|
/// 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<u8>) {
|
|||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
let o = 56 + toc_off + i * 4;
|
let o = 56 + toc_off + i * 4;
|
||||||
let (ko, vo) = (le16(b, o), le16(b, o + 2));
|
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();
|
let mut nb = b.clone();
|
||||||
nb[32..34].copy_from_slice(&(flags & !4).to_le_bytes());
|
nb[32..34].copy_from_slice(&(flags & !4).to_le_bytes());
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ fn dir() -> Option<PathBuf> {
|
|||||||
d.join("images.conf").exists().then_some(d)
|
d.join("images.conf").exists().then_some(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn conf(d: &PathBuf) -> BTreeMap<String, BTreeMap<String, String>> {
|
fn conf(d: &std::path::Path) -> BTreeMap<String, BTreeMap<String, String>> {
|
||||||
let mut m: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
|
let mut m: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
|
||||||
let mut cur = String::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();
|
let l = l.trim();
|
||||||
if let Some(n) = l.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
|
if let Some(n) = l.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
|
||||||
cur = n.to_string();
|
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 head = read_vec(src, 0, 4096).unwrap();
|
||||||
let fs = detect_fs_in(&head);
|
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]
|
#[test]
|
||||||
@@ -66,19 +73,23 @@ fn opens_all_reference_images_with_password_and_recovery_key() {
|
|||||||
}
|
}
|
||||||
let mut views = vec![];
|
let mut views = vec![];
|
||||||
if !pw.is_empty() {
|
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)"));
|
check_fs(u.source.as_ref(), &format!("{name} (senha)"));
|
||||||
views.push(u);
|
views.push(u);
|
||||||
}
|
}
|
||||||
if !rp.is_empty() {
|
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)"));
|
check_fs(u.source.as_ref(), &format!("{name} (recuperação)"));
|
||||||
views.push(u);
|
views.push(u);
|
||||||
}
|
}
|
||||||
if let Some(g) = c.get("SK_VMK_GUID") {
|
if let Some(g) = c.get("SK_VMK_GUID") {
|
||||||
let f = d.join(format!("{}.BEK", g.to_uppercase()));
|
let f = d.join(format!("{}.BEK", g.to_uppercase()));
|
||||||
if f.exists() {
|
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)"));
|
check_fs(u.source.as_ref(), &format!("{name} (BEK)"));
|
||||||
views.push(u);
|
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");
|
assert!(!views.is_empty(), "{name}: nenhum protetor testável");
|
||||||
let first = read_vec(views[0].source.as_ref(), 0, 1 << 20).unwrap();
|
let first = read_vec(views[0].source.as_ref(), 0, 1 << 20).unwrap();
|
||||||
for v in &views[1..] {
|
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;
|
opened += 1;
|
||||||
eprintln!("ok {name}: {} ({} protetores)", views[0].description, views.len());
|
eprintln!(
|
||||||
|
"ok {name}: {} ({} protetores)",
|
||||||
|
views[0].description,
|
||||||
|
views.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
assert!(opened > 0);
|
assert!(opened > 0);
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,17 @@
|
|||||||
mod common;
|
mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
use guara_core::fs::btrfs::{crc32c, live_paths, lzo1x_decompress};
|
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 guara_core::*;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn have(t: &str) -> bool {
|
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<u8> {
|
fn data(seed: u64, n: usize) -> Vec<u8> {
|
||||||
@@ -51,10 +55,22 @@ fn build() -> Option<Img> {
|
|||||||
std::fs::write(root.join(p), c).unwrap();
|
std::fs::write(root.join(p), c).unwrap();
|
||||||
}
|
}
|
||||||
let path = dir.path().join("b.img");
|
let path = dir.path().join("b.img");
|
||||||
std::fs::File::create(&path).unwrap().set_len(128 << 20).unwrap();
|
std::fs::File::create(&path)
|
||||||
let o = Command::new("mkfs.btrfs").args(["-q", "-f", "--rootdir"]).arg(&root).arg(&path).output().unwrap();
|
.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));
|
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
|
||||||
Some(Img { _dir: dir, path, files })
|
Some(Img {
|
||||||
|
_dir: dir,
|
||||||
|
path,
|
||||||
|
files,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -63,7 +79,10 @@ fn lists_the_live_tree_with_paths_and_sizes() {
|
|||||||
let src = FileSource::open(&i.path).unwrap();
|
let src = FileSource::open(&i.path).unwrap();
|
||||||
let listed = live_paths(&src, 0);
|
let listed = live_paths(&src, 0);
|
||||||
for (p, c) in &i.files {
|
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_eq!(f.1, c.len() as u64, "{p}");
|
||||||
assert!(!f.2);
|
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 root_leaf = (t.root_tree as usize, t.root_tree_logical);
|
||||||
let ext_leaf = (t.extent_tree as usize, t.extent_tree_logical);
|
let ext_leaf = (t.extent_tree as usize, t.extent_tree_logical);
|
||||||
let to_logical = |phys: usize| -> u64 {
|
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)
|
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
|
// inode do arquivo apagado
|
||||||
let victim = "relatorio.dat";
|
let victim = "relatorio.dat";
|
||||||
let mut ino = 0;
|
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.
|
// 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)
|
let free_slot = (0..img.len() / nodesize)
|
||||||
.map(|n| n * 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");
|
.expect("bloco livre");
|
||||||
// logical = bytenr da folha antiga + (offset físico - offset físico antigo): mesmo chunk
|
// logical = bytenr da folha antiga + (offset físico - offset físico antigo): mesmo chunk
|
||||||
let new_bytenr = to_logical(free_slot);
|
let new_bytenr = to_logical(free_slot);
|
||||||
let newleaf = fl.without(|o, _| o == ino, new_bytenr);
|
let newleaf = fl.without(|o, _| o == ino, new_bytenr);
|
||||||
img[free_slot..free_slot + nodesize].copy_from_slice(&newleaf.raw);
|
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.
|
// 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() {
|
for k in 0..rl.nritems() {
|
||||||
let (obj, ty, _, off, _) = rl.item(k);
|
let (obj, ty, _, off, _) = rl.item(k);
|
||||||
if obj == 5 && ty == 132 {
|
if obj == 5 && ty == 132 {
|
||||||
@@ -212,7 +246,9 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
|
|||||||
rl.seal();
|
rl.seal();
|
||||||
img[root_leaf.0..root_leaf.0 + nodesize].copy_from_slice(&rl.raw);
|
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).
|
// 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 newext = el.without(|o, ty| o == data_bytenr && ty == 168, ext_leaf.1);
|
||||||
let _ = to_logical;
|
let _ = to_logical;
|
||||||
img[ext_leaf.0..ext_leaf.0 + nodesize].copy_from_slice(&newext.raw);
|
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");
|
let p2 = i._dir.path().join("deleted.img");
|
||||||
std::fs::write(&p2, &img).unwrap();
|
std::fs::write(&p2, &img).unwrap();
|
||||||
init();
|
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
|
let f = c
|
||||||
.found
|
.found
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.name == victim && f.origin == Origin::Btrfs)
|
.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::<Vec<_>>()));
|
.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"não achou: {:?}",
|
||||||
|
c.found
|
||||||
|
.iter()
|
||||||
|
.map(|f| (&f.name, f.origin))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
assert_eq!(f.size, 150_000);
|
assert_eq!(f.size, 150_000);
|
||||||
assert_eq!(f.dir.as_deref(), Some("/docs"));
|
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 out = i._dir.path().join("out");
|
||||||
let rep = recover_all(&p2, std::slice::from_ref(f), &out);
|
let rep = recover_all(&p2, std::slice::from_ref(f), &out);
|
||||||
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
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();
|
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
|
||||||
assert!(got == i.files[0].1);
|
assert!(got == i.files[0].1);
|
||||||
// Os arquivos que continuam existindo não aparecem como apagados.
|
// 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("");
|
let _ = Path::new("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +304,10 @@ fn lzo_literals_and_a_match() {
|
|||||||
let mut s = vec![17 + 20];
|
let mut s = vec![17 + 20];
|
||||||
s.extend((0..20u8).map(|i| b'a' + i));
|
s.extend((0..20u8).map(|i| b'a' + i));
|
||||||
s.extend_from_slice(&[0x11, 0, 0]);
|
s.extend_from_slice(&[0x11, 0, 0]);
|
||||||
assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), (0..20u8).map(|i| b'a' + i).collect::<Vec<_>>());
|
assert_eq!(
|
||||||
|
lzo1x_decompress(&s, 4096).unwrap(),
|
||||||
|
(0..20u8).map(|i| b'a' + i).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
// "abcd" + M2 (dist 4, len 4) + fim
|
// "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];
|
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");
|
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 z = e.finish().unwrap();
|
||||||
let mut disk = vec![0u8; 4096];
|
let mut disk = vec![0u8; 4096];
|
||||||
disk.extend_from_slice(&z);
|
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]);
|
assert_eq!(p.read(&MemSource(disk)).unwrap(), &plain[1000..6000]);
|
||||||
// zstd, se o utilitário existir
|
// zstd, se o utilitário existir
|
||||||
if have("zstd") {
|
if have("zstd") {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let f = dir.path().join("p.bin");
|
let f = dir.path().join("p.bin");
|
||||||
std::fs::write(&f, &plain).unwrap();
|
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 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);
|
assert!(p.read(&MemSource(z)).unwrap() == plain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,15 +17,23 @@ fn photo_n(w: u16, h: u16, seed: u64, shift: i32, nmask: u64) -> Vec<u8> {
|
|||||||
let noise = ((x >> 40) & nmask) as i32;
|
let noise = ((x >> 40) & nmask) as i32;
|
||||||
let fx = xx as f32 / w as f32;
|
let fx = xx as f32 / w as f32;
|
||||||
let fy = yy as f32 / h 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 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;
|
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]);
|
rgb.extend_from_slice(&[v, v.saturating_sub(10), (v as i32 / 2 + noise) as u8]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut out = vec![];
|
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
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +79,10 @@ fn rebuilds_a_jpeg_split_around_another_one() {
|
|||||||
// O fragmento correto precisa estar marcado.
|
// O fragmento correto precisa estar marcado.
|
||||||
assert!(idx.get(a2_off));
|
assert!(idx.get(a2_off));
|
||||||
eprintln!("TRUE a2_off={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) {
|
match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) {
|
||||||
Analysis::Rebuilt { extents, size } => {
|
Analysis::Rebuilt { extents, size } => {
|
||||||
assert_eq!(size as usize, a.len(), "tamanho");
|
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[0], Extent::new(a_off, split as u64));
|
||||||
assert_eq!(extents[1].offset, a2_off);
|
assert_eq!(extents[1].offset, a2_off);
|
||||||
let mut got = disk[a_off as usize..a_off as usize + split].to_vec();
|
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");
|
assert!(got == a, "conteúdo recomposto difere");
|
||||||
}
|
}
|
||||||
other => panic!("esperava Rebuilt, veio {other:?}"),
|
other => panic!("esperava Rebuilt, veio {other:?}"),
|
||||||
}
|
}
|
||||||
// O outro JPEG, íntegro, continua íntegro.
|
// 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) {
|
match analyze_jpeg(&src, b_off, 4 << 20, &pool2, true) {
|
||||||
Analysis::Intact { len } => assert_eq!(len as usize, b.len()),
|
Analysis::Intact { len } => assert_eq!(len as usize, b.len()),
|
||||||
other => panic!("B deveria estar íntegro: {other:?}"),
|
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
|
.filter(|(i, _)| *i != 2) // o fragmento do meio de A não é de outro arquivo
|
||||||
.map(|(_, (o, l))| Extent::new(*o, *l))
|
.map(|(_, (o, l))| Extent::new(*o, *l))
|
||||||
.collect();
|
.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) {
|
match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) {
|
||||||
Analysis::Rebuilt { extents, size } => {
|
Analysis::Rebuilt { extents, size } => {
|
||||||
assert_eq!(size as usize, n);
|
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 dir = tempfile::tempdir().unwrap();
|
||||||
let img = dir.path().join("d.img");
|
let img = dir.path().join("d.img");
|
||||||
std::fs::write(&img, &disk).unwrap();
|
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();
|
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::<Vec<_>>());
|
assert_eq!(
|
||||||
let fa = jpgs.iter().find(|f| f.first_offset() == Some(a_off as u64)).unwrap();
|
jpgs.len(),
|
||||||
|
2,
|
||||||
|
"{:?}",
|
||||||
|
jpgs.iter()
|
||||||
|
.map(|f| (f.size, f.first_offset(), f.confidence))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
let fa = jpgs
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.first_offset() == Some(a_off as u64))
|
||||||
|
.unwrap();
|
||||||
assert_eq!(fa.size as usize, n, "{:?}", fa.note);
|
assert_eq!(fa.size as usize, n, "{:?}", fa.note);
|
||||||
let out = dir.path().join("out");
|
let out = dir.path().join("out");
|
||||||
let rep = common::recover_all(&img, std::slice::from_ref(*fa), &out);
|
let rep = common::recover_all(&img, std::slice::from_ref(*fa), &out);
|
||||||
let p = std::path::PathBuf::from(&rep.recovered[0].path);
|
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();
|
let got = std::fs::read(if p.is_absolute() { p } else { out.join(p) }).unwrap();
|
||||||
assert!(got == a, "JPEG recuperado difere do original");
|
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());
|
assert_eq!(fb.size as usize, photo_n(1600, 1200, 77, 30, 3).len());
|
||||||
let _ = (b_len, recover::RecoverReport::default());
|
let _ = (b_len, recover::RecoverReport::default());
|
||||||
}
|
}
|
||||||
@@ -11,13 +11,26 @@ use std::process::{Command, Stdio};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn have(t: &str) -> bool {
|
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 {
|
fn run_stdin(c: &mut Command, input: &str) -> bool {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let mut ch = c.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn().unwrap();
|
let mut ch = c
|
||||||
ch.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();
|
.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()
|
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/segredo.dat"), &secret_doc).unwrap();
|
||||||
std::fs::write(root.join("docs/fica.dat"), data(12, 5000)).unwrap();
|
std::fs::write(root.join("docs/fica.dat"), data(12, 5000)).unwrap();
|
||||||
let plain = dir.path().join("plain.img");
|
let plain = dir.path().join("plain.img");
|
||||||
std::fs::File::create(&plain).unwrap().set_len(8 << 20).unwrap();
|
std::fs::File::create(&plain)
|
||||||
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success());
|
.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| {
|
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");
|
dbg("rm /docs/segredo.dat");
|
||||||
|
|
||||||
let img = dir.path().join("luks.img");
|
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 pw = "correct horse";
|
||||||
let mut fmt = Command::new("cryptsetup");
|
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));
|
assert!(run_stdin(&mut fmt, pw));
|
||||||
let vkf = dir.path().join("vk");
|
let vkf = dir.path().join("vk");
|
||||||
let mut d = Command::new("cryptsetup");
|
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));
|
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 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")
|
assert!(Command::new("python3")
|
||||||
.arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/support/luks_encrypt.py"))
|
.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")
|
.arg(&img)
|
||||||
.status().unwrap().success());
|
.arg(&plain)
|
||||||
|
.arg(&vkf)
|
||||||
|
.arg(off.to_string())
|
||||||
|
.arg("aes-xts-plain64")
|
||||||
|
.arg("512")
|
||||||
|
.status()
|
||||||
|
.unwrap()
|
||||||
|
.success());
|
||||||
|
|
||||||
let base: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
|
let base: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
|
||||||
// Sem senha: o volume segue fechado e a varredura não inventa arquivos.
|
// 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.unlocked.len(), 0);
|
||||||
assert_eq!(locked.locked.len(), 1);
|
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();
|
let open = assemble_source(base, &asm).unwrap();
|
||||||
assert_eq!(open.unlocked.len(), 1);
|
assert_eq!(open.unlocked.len(), 1);
|
||||||
let mut sink = Collect::default();
|
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));
|
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::<Vec<_>>()));
|
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::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
assert_eq!(f.size, 120_000);
|
assert_eq!(f.size, 120_000);
|
||||||
let dest = tempfile::tempdir().unwrap();
|
let dest = tempfile::tempdir().unwrap();
|
||||||
let refs = vec![f];
|
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());
|
assert!(rep.failed.is_empty());
|
||||||
let p = &rep.recovered[0].path;
|
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);
|
assert!(std::fs::read(p).unwrap() == secret_doc);
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,11 @@ use std::process::Command;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn have(t: &str) -> bool {
|
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<u8> {
|
fn data(seed: u64, n: usize) -> Vec<u8> {
|
||||||
@@ -67,9 +71,24 @@ fn deleted_file_on_degraded_raid5() {
|
|||||||
let dd = n - 1;
|
let dd = n - 1;
|
||||||
let rows = 40usize;
|
let rows = 40usize;
|
||||||
let logical_len = rows * dd * CHUNK;
|
let logical_len = rows * dd * CHUNK;
|
||||||
std::fs::File::create(&plain).unwrap().set_len(logical_len as u64).unwrap();
|
std::fs::File::create(&plain)
|
||||||
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success());
|
.unwrap()
|
||||||
Command::new("debugfs").arg("-w").arg("-R").arg("rm /fotos/ferias.dat").arg(&plain).output().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();
|
let logical = std::fs::read(&plain).unwrap();
|
||||||
|
|
||||||
// RAID 5 left-symmetric
|
// RAID 5 left-symmetric
|
||||||
@@ -101,24 +120,56 @@ fn deleted_file_on_degraded_raid5() {
|
|||||||
}
|
}
|
||||||
let base: Arc<dyn Source> = Arc::new(FileSource::open(&paths[0]).unwrap());
|
let base: Arc<dyn Source> = Arc::new(FileSource::open(&paths[0]).unwrap());
|
||||||
let asm = Assembly {
|
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()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let a = assemble_source(base, &asm).unwrap();
|
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.len(), 1);
|
||||||
assert_eq!(a.extra_parts[0].fs, FsKind::Ext);
|
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 mut sink = Collect::default();
|
||||||
let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink);
|
let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink);
|
||||||
assert!(matches!(out, Outcome::Completed));
|
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::<Vec<_>>()));
|
let f = sink
|
||||||
|
.found
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == "ferias.dat")
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"{:?}",
|
||||||
|
sink.found.iter().map(|f| &f.name).collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
assert_eq!(f.size, 300_000);
|
assert_eq!(f.size, 300_000);
|
||||||
let dest = tempfile::tempdir().unwrap();
|
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);
|
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
||||||
let p = &rep.recovered[0].path;
|
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);
|
assert!(std::fs::read(p).unwrap() == doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +184,11 @@ fn raid5_without_metadata_is_detected_and_scanned() {
|
|||||||
for d in 0..10 {
|
for d in 0..10 {
|
||||||
std::fs::create_dir_all(root.join(format!("d{d}"))).unwrap();
|
std::fs::create_dir_all(root.join(format!("d{d}"))).unwrap();
|
||||||
for f in 0..30 {
|
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);
|
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 (n, chunk) = (3usize, 64 * 1024usize);
|
||||||
let dd = n - 1;
|
let dd = n - 1;
|
||||||
let rows = 160usize;
|
let rows = 160usize;
|
||||||
std::fs::File::create(&plain).unwrap().set_len((rows * dd * chunk) as u64).unwrap();
|
std::fs::File::create(&plain)
|
||||||
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success());
|
.unwrap()
|
||||||
Command::new("debugfs").arg("-w").arg("-R").arg("rm /d3/perdido.dat").arg(&plain).output().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();
|
let logical = std::fs::read(&plain).unwrap();
|
||||||
// left-asymmetric
|
// left-asymmetric
|
||||||
let mut disks = vec![vec![0u8; rows * chunk]; n];
|
let mut disks = vec![vec![0u8; rows * chunk]; n];
|
||||||
@@ -169,21 +239,48 @@ fn raid5_without_metadata_is_detected_and_scanned() {
|
|||||||
}
|
}
|
||||||
let base: Arc<dyn Source> = Arc::new(FileSource::open(&paths[0]).unwrap());
|
let base: Arc<dyn Source> = Arc::new(FileSource::open(&paths[0]).unwrap());
|
||||||
let asm = Assembly {
|
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()),
|
raid: Some(guara_core::assemble::RaidSpec::default()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let a = assemble_source(base, &asm).unwrap();
|
let a = assemble_source(base, &asm).unwrap();
|
||||||
assert!(a.skip_primary);
|
assert!(a.skip_primary);
|
||||||
assert!(a.notes.iter().any(|n| n.contains("descobertos")), "{:?}", a.notes);
|
assert!(
|
||||||
let opts = ScanOptions { mode: ScanMode::Deep, extra_parts: a.extra_parts.clone(), skip_primary: true, ..Default::default() };
|
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 mut sink = Collect::default();
|
||||||
let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink);
|
let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink);
|
||||||
assert!(matches!(out, Outcome::Completed));
|
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 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 = &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);
|
assert!(std::fs::read(p).unwrap() == doc);
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,9 @@ use guara_core::source::{open_path, read_vec, Source};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn fx(name: &str) -> 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<u8> {
|
fn raw() -> Vec<u8> {
|
||||||
@@ -32,8 +34,14 @@ fn stored_hash_matches() {
|
|||||||
}
|
}
|
||||||
let e = EwfSource::open(&fx("s_e6.E01")).unwrap();
|
let e = EwfSource::open(&fx("s_e6.E01")).unwrap();
|
||||||
assert_eq!(e.info.version, 1);
|
assert_eq!(e.info.version, 1);
|
||||||
assert_eq!(e.info.fields.get("Examinador").map(String::as_str), Some("examiner"));
|
assert_eq!(
|
||||||
assert_eq!(e.info.fields.get("Número do caso").map(String::as_str), Some("case1"));
|
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);
|
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()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let rep = write_e01(&src, &base, &opts, &|| false, &mut |_, _| {}).unwrap();
|
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();
|
let r = open_path(&dir.path().join("img.E01")).unwrap();
|
||||||
assert_eq!(r.size(), data.len() as u64);
|
assert_eq!(r.size(), data.len() as u64);
|
||||||
assert!(read_vec(r.as_ref(), 0, data.len()).unwrap() == data);
|
assert!(read_vec(r.as_ref(), 0, data.len()).unwrap() == data);
|
||||||
let e = EwfSource::open(&dir.path().join("img.E01")).unwrap();
|
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.info.md5.as_deref(), Some(rep.md5.as_str()));
|
||||||
assert_eq!(e.verify().unwrap(), Some(true));
|
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]
|
#[test]
|
||||||
@@ -74,7 +88,17 @@ fn compressed_output_is_smaller_and_odd_sizes_work() {
|
|||||||
let src = guara_core::source::MemSource(data.clone());
|
let src = guara_core::source::MemSource(data.clone());
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let base = dir.path().join("z");
|
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);
|
assert!(std::fs::metadata(dir.path().join("z.E01")).unwrap().len() < 10_000);
|
||||||
let r = open_path(&dir.path().join("z.E01")).unwrap();
|
let r = open_path(&dir.path().join("z.E01")).unwrap();
|
||||||
assert_eq!(r.size(), 100_352);
|
assert_eq!(r.size(), 100_352);
|
||||||
@@ -85,8 +109,22 @@ fn compressed_output_is_smaller_and_odd_sizes_work() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn emit_for_external_verification() {
|
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 src = guara_core::source::MemSource(raw());
|
||||||
let opts = E01Options { segment_size: 100_000, case_number: "c1".into(), examiner: "ex".into(), ..Default::default() };
|
let opts = E01Options {
|
||||||
write_e01(&src, std::path::Path::new(&base), &opts, &|| false, &mut |_, _| {}).unwrap();
|
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();
|
||||||
}
|
}
|
||||||
+100
-19
@@ -17,7 +17,13 @@ fn have(tool: &str) -> bool {
|
|||||||
fn debugfs(img: &Path, cmds: &[&str]) -> String {
|
fn debugfs(img: &Path, cmds: &[&str]) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for c in cmds {
|
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.stdout));
|
||||||
out.push_str(&String::from_utf8_lossy(&o.stderr));
|
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 {
|
fn inode_of(img: &Path, path: &str) -> u64 {
|
||||||
let o = debugfs(img, &[&format!("stat {path}")]);
|
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()
|
l.split_whitespace().nth(1).unwrap().parse().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,13 +85,24 @@ fn build(opts: &[&str]) -> Option<Img> {
|
|||||||
.arg(&img)
|
.arg(&img)
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(st.status.success(), "{}", String::from_utf8_lossy(&st.stderr));
|
assert!(
|
||||||
Some(Img { _dir: dir, img, files })
|
st.status.success(),
|
||||||
|
"{}",
|
||||||
|
String::from_utf8_lossy(&st.stderr)
|
||||||
|
);
|
||||||
|
Some(Img {
|
||||||
|
_dir: dir,
|
||||||
|
img,
|
||||||
|
files,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deep(img: &Path) -> common::Collect {
|
fn deep(img: &Path) -> common::Collect {
|
||||||
init();
|
init();
|
||||||
let opts = ScanOptions { mode: ScanMode::Deep, ..ScanOptions::default() };
|
let opts = ScanOptions {
|
||||||
|
mode: ScanMode::Deep,
|
||||||
|
..ScanOptions::default()
|
||||||
|
};
|
||||||
scan_path(img, &opts)
|
scan_path(img, &opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,30 +111,60 @@ fn recovered(img: &Path, f: &Found) -> Vec<u8> {
|
|||||||
let rep = recover_all(img, std::slice::from_ref(f), d.path());
|
let rep = recover_all(img, std::slice::from_ref(f), d.path());
|
||||||
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
||||||
let p = &rep.recovered[0].path;
|
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()
|
std::fs::read(p).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deleted_by_debugfs_keeps_extents_and_name() {
|
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"]);
|
debugfs(&i.img, &["rm /docs/relatorio.dat", "rm /fotos/viagem.dat"]);
|
||||||
let c = deep(&i.img);
|
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 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::<Vec<_>>()));
|
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::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
assert_eq!(f.size, data.len() as u64);
|
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}");
|
assert!(recovered(&i.img, f) == *data, "{name}");
|
||||||
}
|
}
|
||||||
// O arquivo que não foi apagado não aparece.
|
// 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]
|
#[test]
|
||||||
fn truncated_inode_recovered_by_remnant_extents() {
|
fn truncated_inode_recovered_by_remnant_extents() {
|
||||||
// Simula o que o kernel faz ao apagar: tamanho zerado e contagem de extents zerada.
|
// 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");
|
let ino = inode_of(&i.img, "/docs/sub/contrato.dat");
|
||||||
debugfs(
|
debugfs(
|
||||||
&i.img,
|
&i.img,
|
||||||
@@ -125,7 +175,11 @@ fn truncated_inode_recovered_by_remnant_extents() {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
let c = deep(&i.img);
|
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);
|
assert_eq!(f.origin, Origin::Ext);
|
||||||
// Tamanho exato é perdido: recupera todos os blocos (9000 bytes arredondados para 3 blocos de 4K).
|
// Tamanho exato é perdido: recupera todos os blocos (9000 bytes arredondados para 3 blocos de 4K).
|
||||||
assert_eq!(f.size, 12288);
|
assert_eq!(f.size, 12288);
|
||||||
@@ -138,7 +192,9 @@ fn journal_copy_restores_exact_size() {
|
|||||||
if !have("debugfs") {
|
if !have("debugfs") {
|
||||||
return;
|
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.
|
// Bloco da tabela de inodes que contém o inode do arquivo.
|
||||||
let ino = inode_of(&i.img, "/fotos/viagem.dat");
|
let ino = inode_of(&i.img, "/fotos/viagem.dat");
|
||||||
let o = debugfs(&i.img, &["stats"]);
|
let o = debugfs(&i.img, &["stats"]);
|
||||||
@@ -157,9 +213,23 @@ fn journal_copy_restores_exact_size() {
|
|||||||
let bin = i.img.with_extension("blk");
|
let bin = i.img.with_extension("blk");
|
||||||
std::fs::write(&bin, old).unwrap();
|
std::fs::write(&bin, old).unwrap();
|
||||||
let script = i.img.with_extension("cmds");
|
let script = i.img.with_extension("cmds");
|
||||||
std::fs::write(&script, format!("jo\njw -b {blk} {}\njc\njc\n", bin.display())).unwrap();
|
std::fs::write(
|
||||||
let o = Command::new("debugfs").arg("-w").arg("-f").arg(&script).arg(&i.img).output().unwrap();
|
&script,
|
||||||
let j = format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr));
|
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") {
|
if j.contains("Unknown request") || j.contains("not supported") {
|
||||||
eprintln!("SKIP: debugfs sem suporte a escrita no diário: {j}");
|
eprintln!("SKIP: debugfs sem suporte a escrita no diário: {j}");
|
||||||
return;
|
return;
|
||||||
@@ -174,7 +244,16 @@ fn journal_copy_restores_exact_size() {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
let c = deep(&i.img);
|
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::<Vec<_>>()));
|
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::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
assert_eq!(f.size, 700_000, "{:?}", f.note);
|
assert_eq!(f.size, 700_000, "{:?}", f.note);
|
||||||
assert!(recovered(&i.img, f) == i.files[2].1);
|
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() {
|
fn ext2_blockmap_deleted_is_not_invented() {
|
||||||
// ext2 zera os ponteiros ao apagar; sem diário não há o que recuperar por metadados,
|
// 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.
|
// 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);
|
let c = deep(&i.img);
|
||||||
assert!(!c.found.iter().any(|f| f.origin == Origin::Ext));
|
assert!(!c.found.iter().any(|f| f.origin == Origin::Ext));
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,8 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn opens_reference_image_and_matches_known_hash() {
|
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() {
|
if !p.exists() {
|
||||||
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||||
return;
|
return;
|
||||||
@@ -31,5 +32,8 @@ fn opens_reference_image_and_matches_known_hash() {
|
|||||||
h.update(&buf[..n]);
|
h.update(&buf[..n]);
|
||||||
off += n as u64;
|
off += n as u64;
|
||||||
}
|
}
|
||||||
assert_eq!(h.finalize().sha256, "2c662e36c0f7e2f5583e6a939bbcbdc660805692d0fccaa45ad4052beb3b8e18");
|
assert_eq!(
|
||||||
|
h.finalize().sha256,
|
||||||
|
"2c662e36c0f7e2f5583e6a939bbcbdc660805692d0fccaa45ad4052beb3b8e18"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -16,31 +16,78 @@ fn check(path: &str, ext: &str, min_conf: Confidence) -> bool {
|
|||||||
// lixo depois do fim real (outro arquivo no disco)
|
// lixo depois do fim real (outro arquivo no disco)
|
||||||
data.extend((0..5000u32).map(|i| (i.wrapping_mul(2654435761) >> 7) as u8 | 0x81));
|
data.extend((0..5000u32).map(|i| (i.wrapping_mul(2654435761) >> 7) as u8 | 0x81));
|
||||||
let src = MemSource(data.clone());
|
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.len as usize, real, "{path}: tamanho");
|
||||||
assert_eq!(h.ext, ext, "{path}: extensão");
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn first(dir: &str, ext: &str) -> Option<String> {
|
fn first(dir: &str, ext: &str) -> Option<String> {
|
||||||
let out = std::process::Command::new("find").args([dir, "-iname", &format!("*.{ext}"), "-type", "f", "-size", "+2k", "-size", "-20M"]).output().ok()?;
|
let out = std::process::Command::new("find")
|
||||||
String::from_utf8_lossy(&out.stdout).lines().next().map(String::from)
|
.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]
|
#[test]
|
||||||
fn real_files_have_exact_lengths() {
|
fn real_files_have_exact_lengths() {
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
let cases: Vec<(Option<String>, &str, Confidence)> = vec![
|
let cases: Vec<(Option<String>, &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", "gz"), "gz", Confidence::High),
|
||||||
(first("/usr/share/doc", "bz2"), "bz2", Confidence::High),
|
(first("/usr/share/doc", "bz2"), "bz2", Confidence::High),
|
||||||
(first("/usr/lib/firmware", "zst"), "zst", Confidence::Medium),
|
(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(
|
||||||
(first("/usr/share/app-install/icons", "tiff"), "tif", Confidence::Medium),
|
"/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),
|
(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 {
|
for (path, ext, conf) in cases {
|
||||||
if let Some(p) = path {
|
if let Some(p) = path {
|
||||||
@@ -55,9 +102,24 @@ fn real_files_have_exact_lengths() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn webm_rar_deb_with_unknown_locations() {
|
fn webm_rar_deb_with_unknown_locations() {
|
||||||
for (dir, ext, out, conf) in [
|
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/Nextcloud/apps/firstrunwizard/img",
|
||||||
("/media/lucas/d9d2ba04-f925-4b9a-97d7-92c2e3bfdb78/war_cloud/lucasrguerra/Backups", "rar", "rar", Confidence::High),
|
"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) {
|
if let Some(p) = first(dir, ext) {
|
||||||
check(&p, out, conf);
|
check(&p, out, conf);
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use common::*;
|
|||||||
use guara_core::*;
|
use guara_core::*;
|
||||||
|
|
||||||
fn image() -> Option<Vec<u8>> {
|
fn image() -> Option<Vec<u8>> {
|
||||||
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())
|
p.exists().then(|| std::fs::read(p).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,7 +15,13 @@ fn scan_bytes(bytes: &[u8]) -> Collect {
|
|||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let p = dir.path().join("h.img");
|
let p = dir.path().join("h.img");
|
||||||
std::fs::write(&p, bytes).unwrap();
|
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]
|
#[test]
|
||||||
@@ -25,7 +32,11 @@ fn pristine_volume_has_no_deleted_files_and_is_recognized() {
|
|||||||
};
|
};
|
||||||
let c = scan_bytes(&img);
|
let c = scan_bytes(&img);
|
||||||
assert_eq!(c.layout.as_ref().unwrap().partitions[0].fs, FsKind::HfsPlus);
|
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::<Vec<_>>());
|
assert!(
|
||||||
|
c.found.is_empty(),
|
||||||
|
"{:?}",
|
||||||
|
c.found.iter().map(|f| &f.name).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -33,13 +44,17 @@ fn record_left_in_leaf_slack_is_recovered() {
|
|||||||
let Some(mut img) = image() else { return };
|
let Some(mut img) = image() else { return };
|
||||||
let bs = 4096usize;
|
let bs = 4096usize;
|
||||||
let node = 186 * bs + bs; // catálogo começa no bloco 186; nó 1 é a única folha
|
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());
|
img[node + 10..node + 12].copy_from_slice(&21u16.to_be_bytes());
|
||||||
// Libera o bloco de dados (bitmap de alocação no bloco 1).
|
// Libera o bloco de dados (bitmap de alocação no bloco 1).
|
||||||
let blk = 278usize;
|
let blk = 278usize;
|
||||||
img[bs + blk / 8] &= !(0x80 >> (blk % 8));
|
img[bs + blk / 8] &= !(0x80 >> (blk % 8));
|
||||||
let c = scan_bytes(&img);
|
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::<Vec<_>>()));
|
let f = c
|
||||||
|
.found
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == "fseventsd-uuid")
|
||||||
|
.unwrap_or_else(|| panic!("{:?}", c.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
||||||
assert_eq!(f.origin, Origin::Hfs);
|
assert_eq!(f.origin, Origin::Hfs);
|
||||||
assert_eq!(f.size, 36);
|
assert_eq!(f.size, 36);
|
||||||
assert_eq!(f.dir.as_deref(), Some("/.fseventsd"));
|
assert_eq!(f.dir.as_deref(), Some("/.fseventsd"));
|
||||||
|
|||||||
@@ -11,14 +11,19 @@ fn photo(w: u16, h: u16) -> Vec<u8> {
|
|||||||
x ^= x << 17;
|
x ^= x << 17;
|
||||||
let noise = ((x >> 40) & 31) as i32;
|
let noise = ((x >> 40) & 31) as i32;
|
||||||
let base = 90 + (xx * 90 / w as i32) + (yy * 60 / h 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;
|
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]);
|
rgb.extend_from_slice(&[v, v.saturating_sub(20), (v as i32 / 2 + noise) as u8]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
let enc = jpeg_encoder::Encoder::new(&mut out, 92);
|
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
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +37,10 @@ fn decodes_whole_image_and_ends_at_eoi() {
|
|||||||
assert_eq!(d.dc.len() as u32, h.total_mcus());
|
assert_eq!(d.dc.len() as u32, h.total_mcus());
|
||||||
// O que sobra depois do último MCU é o EOI.
|
// O que sobra depois do último MCU é o EOI.
|
||||||
let tail = &j[d.state.pos.min(j.len())..];
|
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());
|
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.
|
// 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);
|
let mcu_break = first_discontinuity(&h, &d.dc);
|
||||||
// MCUs bons: os decodificados ao fim do cluster `cut`.
|
// 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.
|
// Retomada com a continuação verdadeira reproduz a decodificação inteira.
|
||||||
let mut joined = j[..cut].to_vec();
|
let mut joined = j[..cut].to_vec();
|
||||||
joined.extend_from_slice(&j[cut..]);
|
joined.extend_from_slice(&j[cut..]);
|
||||||
|
|||||||
+105
-15
@@ -25,8 +25,17 @@ fn py_ok() -> bool {
|
|||||||
|
|
||||||
fn run_stdin(cmd: &mut Command, input: &str) -> bool {
|
fn run_stdin(cmd: &mut Command, input: &str) -> bool {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let mut ch = cmd.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn().unwrap();
|
let mut ch = cmd
|
||||||
ch.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();
|
.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()
|
ch.wait().unwrap().success()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +60,26 @@ struct Case<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn payload_offset(img: &Path, ver2: bool) -> u64 {
|
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();
|
let t = String::from_utf8_lossy(&o.stdout).to_string();
|
||||||
if ver2 {
|
if ver2 {
|
||||||
let l = t.lines().skip_while(|l| !l.starts_with("Data segments")).find(|l| l.contains("offset:")).unwrap();
|
let l = t
|
||||||
l.split(':').nth(1).unwrap().split_whitespace().next().unwrap().parse().unwrap()
|
.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 {
|
} else {
|
||||||
let l = t.lines().find(|l| l.starts_with("Payload offset")).unwrap();
|
let l = t.lines().find(|l| l.starts_with("Payload offset")).unwrap();
|
||||||
l.split(':').nth(1).unwrap().trim().parse::<u64>().unwrap() * 512
|
l.split(':').nth(1).unwrap().trim().parse::<u64>().unwrap() * 512
|
||||||
@@ -72,7 +96,10 @@ fn run_case(c: &Case) {
|
|||||||
let plain = dir.path().join("plain.bin");
|
let plain = dir.path().join("plain.bin");
|
||||||
let data = plain_data(3 * 1024 * 1024 + 4096);
|
let data = plain_data(3 * 1024 * 1024 + 4096);
|
||||||
std::fs::write(&plain, &data).unwrap();
|
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 pw = "senha com acentuação ç";
|
||||||
let mut fmt = Command::new("cryptsetup");
|
let mut fmt = Command::new("cryptsetup");
|
||||||
fmt.args(["luksFormat", "--batch-mode", "--key-file", "-"]);
|
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);
|
assert!(run_stdin(&mut fmt, pw), "luksFormat falhou ({})", c.name);
|
||||||
let vkf = dir.path().join("vk.bin");
|
let vkf = dir.path().join("vk.bin");
|
||||||
let mut dump = Command::new("cryptsetup");
|
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);
|
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 off = payload_offset(&img, c.ver2);
|
||||||
let st = Command::new("python3")
|
let st = Command::new("python3")
|
||||||
.arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/support/luks_encrypt.py"))
|
.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),
|
Err(e) => panic!("{}: erro inesperado {e}", c.name),
|
||||||
Ok(_) => panic!("{}: abriu com senha errada", c.name),
|
Ok(_) => panic!("{}: abriu com senha errada", c.name),
|
||||||
}
|
}
|
||||||
let u = luks::unlock(src.clone(), &[Secret::Password("x".into()), Secret::Password(pw.into())])
|
let u = luks::unlock(
|
||||||
.unwrap_or_else(|e| panic!("{}: {e}", c.name));
|
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();
|
let got = read_vec(u.source.as_ref(), 0, data.len()).unwrap();
|
||||||
assert!(got == data, "{}: conteúdo difere", c.name);
|
assert!(got == data, "{}: conteúdo difere", c.name);
|
||||||
// Também abre com a chave do volume.
|
// Também abre com a chave do volume.
|
||||||
let vk = std::fs::read(&vkf).unwrap();
|
let vk = std::fs::read(&vkf).unwrap();
|
||||||
let u2 = luks::unlock(src, &[Secret::VolumeKey(vk)]).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]
|
#[test]
|
||||||
fn luks1_xts() {
|
fn luks1_xts() {
|
||||||
run_case(&Case {
|
run_case(&Case {
|
||||||
name: "luks1-xts",
|
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",
|
cipher: "aes-xts-plain64",
|
||||||
sector: 512,
|
sector: 512,
|
||||||
ver2: false,
|
ver2: false,
|
||||||
@@ -129,7 +182,18 @@ fn luks1_xts() {
|
|||||||
fn luks1_cbc_essiv_sha1() {
|
fn luks1_cbc_essiv_sha1() {
|
||||||
run_case(&Case {
|
run_case(&Case {
|
||||||
name: "luks1-cbc-essiv",
|
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",
|
cipher: "aes-cbc-essiv:sha256",
|
||||||
sector: 512,
|
sector: 512,
|
||||||
ver2: false,
|
ver2: false,
|
||||||
@@ -140,7 +204,20 @@ fn luks1_cbc_essiv_sha1() {
|
|||||||
fn luks2_argon2id_xts() {
|
fn luks2_argon2id_xts() {
|
||||||
run_case(&Case {
|
run_case(&Case {
|
||||||
name: "luks2-argon2id",
|
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",
|
cipher: "aes-xts-plain64",
|
||||||
sector: 4096,
|
sector: 4096,
|
||||||
ver2: true,
|
ver2: true,
|
||||||
@@ -151,7 +228,20 @@ fn luks2_argon2id_xts() {
|
|||||||
fn luks2_pbkdf2_sector512_aes256() {
|
fn luks2_pbkdf2_sector512_aes256() {
|
||||||
run_case(&Case {
|
run_case(&Case {
|
||||||
name: "luks2-pbkdf2",
|
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",
|
cipher: "aes-xts-plain64",
|
||||||
sector: 512,
|
sector: 512,
|
||||||
ver2: true,
|
ver2: true,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn assembles_real_lvm_image() {
|
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() {
|
if !p.exists() {
|
||||||
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -39,11 +39,20 @@ const SPS: &str = "67f4000d90d9b28283f602d41818190000030001000003003c8f142996";
|
|||||||
const PPS: &str = "68ebec448440";
|
const PPS: &str = "68ebec448440";
|
||||||
|
|
||||||
fn hex(s: &str) -> Vec<u8> {
|
fn hex(s: &str) -> Vec<u8> {
|
||||||
(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).
|
/// 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<u8> {
|
fn slice(
|
||||||
|
idr: bool,
|
||||||
|
frame_num: u32,
|
||||||
|
poc_lsb: u32,
|
||||||
|
payload: usize,
|
||||||
|
seed: u8,
|
||||||
|
sps: &mguara::Sps,
|
||||||
|
) -> Vec<u8> {
|
||||||
let mut b = BitW::new();
|
let mut b = BitW::new();
|
||||||
b.ue(0); // first_mb_in_slice
|
b.ue(0); // first_mb_in_slice
|
||||||
b.ue(if idr { 7 } else { 5 }); // slice_type
|
b.ue(if idr { 7 } else { 5 }); // slice_type
|
||||||
@@ -95,7 +104,14 @@ fn synthetic_opt(gops: usize, inband: bool) -> (Vec<u8>, Vec<usize>, Vec<bool>)
|
|||||||
if i == 0 {
|
if i == 0 {
|
||||||
frame_num = 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);
|
nal(&mut mdat, &s);
|
||||||
if p % 4 == 0 || i == 1 || i == 4 {
|
if p % 4 == 0 || i == 1 || i == 4 {
|
||||||
frame_num += 1;
|
frame_num += 1;
|
||||||
@@ -105,7 +121,10 @@ fn synthetic_opt(gops: usize, inband: bool) -> (Vec<u8>, Vec<usize>, Vec<bool>)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut f = 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(&[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(&((8 + mdat.len()) as u32).to_be_bytes());
|
||||||
f.extend_from_slice(b"mdat");
|
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");
|
let r = mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).expect("reconstrução");
|
||||||
assert_eq!(r.frames, sizes.len());
|
assert_eq!(r.frames, sizes.len());
|
||||||
assert_eq!((r.width, r.height), (320, 240));
|
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 out = assemble(&file, &r);
|
||||||
let size = out.len() as u64;
|
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 track_id = *mp4.tracks().keys().next().unwrap();
|
||||||
let t = &mp4.tracks()[&track_id];
|
let t = &mp4.tracks()[&track_id];
|
||||||
assert_eq!(t.sample_count() as usize, sizes.len());
|
assert_eq!(t.sample_count() as usize, sizes.len());
|
||||||
@@ -164,7 +187,9 @@ fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn rebuilds_real_h264_file_like_the_original_index() {
|
fn rebuilds_real_h264_file_like_the_original_index() {
|
||||||
let Ok(path) = std::env::var("GUARA_MP4_SAMPLE") else {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
let orig = std::fs::read(&path).unwrap();
|
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 mut b = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), rsize).unwrap();
|
||||||
let ta = *a.tracks().keys().next().unwrap();
|
let ta = *a.tracks().keys().next().unwrap();
|
||||||
let tb = *b.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() {
|
for i in 1..=a.tracks()[&ta].sample_count() {
|
||||||
let x = a.read_sample(ta, i).unwrap().unwrap();
|
let x = a.read_sample(ta, i).unwrap().unwrap();
|
||||||
let y = b.read_sample(tb, 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).
|
// O tamanho do mdat declarado vai muito além dos 40 MB que temos (arquivo cortado).
|
||||||
let src = MemSource(file.clone());
|
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).
|
// 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");
|
let cfg = mp4rebuild::config_from_mp4(&MemSource(head.clone()), 0, head.len() as u64)
|
||||||
assert!(mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).is_none(), "sem referência não há como montar o avcC");
|
.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 r = mp4rebuild::rebuild(&src, 0, file.len() as u64, &[cfg]).expect("reconstrução");
|
||||||
let out = assemble(&file, &r);
|
let out = assemble(&file, &r);
|
||||||
let osize = out.len() as u64;
|
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.
|
// Original (índice completo) para comparar as primeiras amostras.
|
||||||
let f = std::fs::File::open(&path).unwrap();
|
let f = std::fs::File::open(&path).unwrap();
|
||||||
let mut a = mp4::Mp4Reader::read_header(std::io::BufReader::new(f), total).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();
|
let vid = a
|
||||||
eprintln!("reconstruído: {} quadros, {} bytes pulados (áudio), {}x{}", r.frames, r.skipped_bytes, r.width, r.height);
|
.tracks()
|
||||||
let sizes_a: Vec<usize> = (1..=60).map(|i| a.read_sample(vid, i).unwrap().unwrap().bytes.len()).collect();
|
.values()
|
||||||
let sizes_b: Vec<usize> = (1..=60).map(|i| b.read_sample(tb, i).unwrap().unwrap().bytes.len()).collect();
|
.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<usize> = (1..=60)
|
||||||
|
.map(|i| a.read_sample(vid, i).unwrap().unwrap().bytes.len())
|
||||||
|
.collect();
|
||||||
|
let sizes_b: Vec<usize> = (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]);
|
eprintln!("ORIG {:?}\nMINE {:?}", &sizes_a[40..60], &sizes_b[40..60]);
|
||||||
let check = (r.frames as u32 - 1).min(400);
|
let check = (r.frames as u32 - 1).min(400);
|
||||||
for i in 1..=check {
|
for i in 1..=check {
|
||||||
@@ -275,7 +323,8 @@ fn deep_scan_rebuilds_a_video_without_moov_using_a_neighbour_as_reference() {
|
|||||||
common::init();
|
common::init();
|
||||||
// Arquivo íntegro (com moov) da "mesma câmera" e um segundo que perdeu o moov e não traz SPS/PPS.
|
// 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 (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 good = assemble(&good_raw, &r);
|
||||||
let (broken, sizes, _) = synthetic_opt(10, false);
|
let (broken, sizes, _) = synthetic_opt(10, false);
|
||||||
let mut disk = vec![0u8; 8192];
|
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 dir = tempfile::tempdir().unwrap();
|
||||||
let img = dir.path().join("v.img");
|
let img = dir.path().join("v.img");
|
||||||
std::fs::write(&img, &disk).unwrap();
|
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();
|
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::<Vec<_>>());
|
assert_eq!(
|
||||||
let fb = vids.iter().find(|f| matches!(f.data, guara_core::DataLoc::Parts { .. })).expect("vídeo quebrado");
|
vids.len(),
|
||||||
|
2,
|
||||||
|
"{:?}",
|
||||||
|
vids.iter()
|
||||||
|
.map(|f| (f.first_offset(), f.size, f.confidence, &f.note))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
let fb = vids
|
||||||
|
.iter()
|
||||||
|
.find(|f| matches!(f.data, guara_core::DataLoc::Parts { .. }))
|
||||||
|
.expect("vídeo quebrado");
|
||||||
let _ = broken_off;
|
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 _ = good_off;
|
||||||
let out = dir.path().join("out");
|
let out = dir.path().join("out");
|
||||||
let rep = common::recover_all(&img, std::slice::from_ref(*fb), &out);
|
let rep = common::recover_all(&img, std::slice::from_ref(*fb), &out);
|
||||||
|
|||||||
@@ -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 };
|
let start = if level == Level::R6 { qd + 1 } else { pd + 1 };
|
||||||
(0..dd).map(|d| (start + d) % n).collect()
|
(0..dd).map(|d| (start + d) % n).collect()
|
||||||
} else {
|
} 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 p = vec![0u8; C];
|
||||||
let mut q = vec![0u8; C];
|
let mut q = vec![0u8; C];
|
||||||
@@ -83,7 +85,10 @@ fn members(imgs: &[Vec<u8>], missing: &[usize]) -> Vec<Option<Member>> {
|
|||||||
imgs.iter()
|
imgs.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, v)| {
|
.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()
|
.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 dd = if level == Level::R6 { n - 2 } else { n - 1 };
|
||||||
let logical = data(C * dd * 23);
|
let logical = data(C * dd * 23);
|
||||||
let imgs = encode_parity(level, layout, n, &logical);
|
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);
|
assert_eq!(r.size(), logical.len() as u64);
|
||||||
let got = read_vec(&r, 0, logical.len()).unwrap();
|
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
|
// leitura desalinhada
|
||||||
let part = read_vec(&r, 5000, 30000).unwrap();
|
let part = read_vec(&r, 5000, 30000).unwrap();
|
||||||
assert_eq!(part, &logical[5000..35000]);
|
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() {
|
for (c, ch) in logical.chunks(C).enumerate() {
|
||||||
imgs[c % 3].extend_from_slice(ch);
|
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);
|
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||||
// RAID 1, um membro caído
|
// RAID 1, um membro caído
|
||||||
let m = vec![logical.clone(), logical.clone()];
|
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);
|
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||||
// Linear
|
// Linear
|
||||||
let (a, b) = logical.split_at(C * 10);
|
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);
|
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||||
// RAID 10 near=2 com 4 discos: slots em sequência.
|
// RAID 10 near=2 com 4 discos: slots em sequência.
|
||||||
let n = 4;
|
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);
|
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.near = 2;
|
||||||
p.far = 1;
|
p.far = 1;
|
||||||
let r = RaidSource::new(p).unwrap();
|
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);
|
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<u8> {
|
#[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<u8> {
|
||||||
let mut b = vec![0u8; 4096];
|
let mut b = vec![0u8; 4096];
|
||||||
b[0..4].copy_from_slice(&0xA92B_4EFCu32.to_le_bytes());
|
b[0..4].copy_from_slice(&0xA92B_4EFCu32.to_le_bytes());
|
||||||
b[4..8].copy_from_slice(&1u32.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
|
continue; // disco perdido
|
||||||
}
|
}
|
||||||
let mut full = vec![0u8; 4096 + (data_off as usize) * 512 + im.len()];
|
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..8192].copy_from_slice(&sb);
|
||||||
full[4096 + data_off as usize * 512..][..im.len()].copy_from_slice(im);
|
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
|
// o superbloco 1.2 fica a 4 KiB; os dados começam em data_offset a partir do início
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ use std::process::Command;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn have(t: &str) -> bool {
|
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<Vec<u8>> {
|
fn ext4_image(mib: u64) -> Option<Vec<u8>> {
|
||||||
@@ -19,18 +23,33 @@ fn ext4_image(mib: u64) -> Option<Vec<u8>> {
|
|||||||
for d in 0..12 {
|
for d in 0..12 {
|
||||||
std::fs::create_dir_all(root.join(format!("pasta{d}/sub"))).unwrap();
|
std::fs::create_dir_all(root.join(format!("pasta{d}/sub"))).unwrap();
|
||||||
for f in 0..40 {
|
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");
|
let img = dir.path().join("e.img");
|
||||||
std::fs::File::create(&img).unwrap().set_len(mib << 20).unwrap();
|
std::fs::File::create(&img)
|
||||||
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-b", "1024", "-d"]).arg(&root).arg(&img).status().unwrap().success());
|
.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())
|
Some(std::fs::read(img).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn finds_raid5_chunk_layout_and_shuffled_order() {
|
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 (n, chunk) = (3usize, 32768usize);
|
||||||
let dd = n - 1;
|
let dd = n - 1;
|
||||||
let rows = logical.len() / (chunk * dd);
|
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);
|
disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p);
|
||||||
}
|
}
|
||||||
// Entrega os discos embaralhados: fornecidos = [2, 0, 1] do array real.
|
// Entrega os discos embaralhados: fornecidos = [2, 0, 1] do array real.
|
||||||
let given: Vec<Arc<dyn Source>> = [2usize, 0, 1].iter().map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>).collect();
|
let given: Vec<Arc<dyn Source>> = [2usize, 0, 1]
|
||||||
|
.iter()
|
||||||
|
.map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>)
|
||||||
|
.collect();
|
||||||
let found = raid_detect::detect(&given, &|| false);
|
let found = raid_detect::detect(&given, &|| false);
|
||||||
assert!(!found.is_empty());
|
assert!(!found.is_empty());
|
||||||
for c in found.iter().take(6) {
|
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];
|
let best = &found[0];
|
||||||
assert_eq!(best.level, Level::R5);
|
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.
|
// 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 r = guara_core::raid::RaidSource::new(best.params(&given)).unwrap();
|
||||||
let got = guara_core::source::read_vec(&r, 0, logical.len()).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]
|
#[test]
|
||||||
fn finds_raid0() {
|
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 (n, chunk) = (2usize, 65536usize);
|
||||||
let rows = logical.len() / (chunk * n);
|
let rows = logical.len() / (chunk * n);
|
||||||
let mut disks = vec![vec![]; n];
|
let mut disks = vec![vec![]; n];
|
||||||
for c in 0..rows * n {
|
for c in 0..rows * n {
|
||||||
disks[c % n].extend_from_slice(&logical[c * chunk..(c + 1) * chunk]);
|
disks[c % n].extend_from_slice(&logical[c * chunk..(c + 1) * chunk]);
|
||||||
}
|
}
|
||||||
let given: Vec<Arc<dyn Source>> = [1usize, 0].iter().map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>).collect();
|
let given: Vec<Arc<dyn Source>> = [1usize, 0]
|
||||||
|
.iter()
|
||||||
|
.map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>)
|
||||||
|
.collect();
|
||||||
let found = raid_detect::detect(&given, &|| false);
|
let found = raid_detect::detect(&given, &|| false);
|
||||||
let best = &found[0];
|
let best = &found[0];
|
||||||
assert_eq!(best.level, Level::R0);
|
assert_eq!(best.level, Level::R0);
|
||||||
|
|||||||
+102
-16
@@ -8,7 +8,11 @@ use std::path::PathBuf;
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn have(t: &str) -> bool {
|
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<u8> {
|
fn data(seed: u64, n: usize) -> Vec<u8> {
|
||||||
@@ -36,7 +40,11 @@ fn be64(b: &[u8], o: usize) -> u64 {
|
|||||||
fn unpack(r: &[u8]) -> (u64, u64, u64) {
|
fn unpack(r: &[u8]) -> (u64, u64, u64) {
|
||||||
let hi = be64(r, 0);
|
let hi = be64(r, 0);
|
||||||
let lo = be64(r, 8);
|
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]
|
#[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();
|
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 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();
|
names.sort();
|
||||||
for n in &names {
|
for n in &names {
|
||||||
proto.push_str(&format!(" {n} ---644 0 0 {}\n", src.join(n).display()));
|
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");
|
let pf = dir.path().join("proto");
|
||||||
std::fs::write(&pf, proto).unwrap();
|
std::fs::write(&pf, proto).unwrap();
|
||||||
let img = dir.path().join("x.img");
|
let img = dir.path().join("x.img");
|
||||||
std::fs::File::create(&img).unwrap().set_len(320 << 20).unwrap();
|
std::fs::File::create(&img)
|
||||||
let o = Command::new("mkfs.xfs").args(["-q", "-f", "-p"]).arg(&pf).arg(&img).output().unwrap();
|
.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));
|
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
|
||||||
|
|
||||||
let db = |cmds: &[&str]| -> String {
|
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()
|
String::from_utf8_lossy(&c.output().unwrap().stdout).to_string()
|
||||||
};
|
};
|
||||||
let nc = db(&["blockget -n", "ncheck"]);
|
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 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 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 (agblocks, agblklog, inopblog) = (num("agblocks"), num("agblklog"), num("inopblog"));
|
||||||
|
|
||||||
let mut b = std::fs::read(&img).unwrap();
|
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)
|
// extents do arquivo (no inode, formato 3)
|
||||||
assert_eq!(b[ino_off + 5], 2);
|
assert_eq!(b[ino_off + 5], 2);
|
||||||
let nex = be32(&b[ino_off..], 76) as usize;
|
let nex = be32(&b[ino_off..], 76) as usize;
|
||||||
let exts: Vec<_> = (0..nex).map(|i| unpack(&b[ino_off + 176 + i * 16..])).collect();
|
let exts: Vec<_> = (0..nex)
|
||||||
assert_eq!(exts.iter().map(|e| e.2).sum::<u64>() * 4096 >= 150_000, true);
|
.map(|i| unpack(&b[ino_off + 176 + i * 16..]))
|
||||||
|
.collect();
|
||||||
|
assert!(exts.iter().map(|e| e.2).sum::<u64>() * 4096 >= 150_000);
|
||||||
// 1) libera o inode como o kernel: modo 0, links 0, tamanho 0, 0 extents
|
// 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 + 2..ino_off + 4].fill(0);
|
||||||
b[ino_off + 16..ino_off + 20].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);
|
assert_eq!(blevel, 1);
|
||||||
let bb = ag_off + broot * bs;
|
let bb = ag_off + broot * bs;
|
||||||
let nrec = be16(&b[bb..], 6) as usize;
|
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 {
|
for e in &exts {
|
||||||
recs.push(((e.1 & ((1 << agblklog) - 1)) as u32, e.2 as u32));
|
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());
|
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)
|
// 4) entrada de diretório liberada: perde os 4 primeiros bytes do inode (viram marca + tamanho)
|
||||||
let name = b"relatorio.dat";
|
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;
|
let size = (8 + 1 + name.len() + 1 + 2usize).div_ceil(8) * 8;
|
||||||
b[pos..pos + 2].copy_from_slice(&[0xFF, 0xFF]);
|
b[pos..pos + 2].copy_from_slice(&[0xFF, 0xFF]);
|
||||||
b[pos + 2..pos + 4].copy_from_slice(&(size as u16).to_be_bytes());
|
b[pos + 2..pos + 4].copy_from_slice(&(size as u16).to_be_bytes());
|
||||||
std::fs::write(&img, &b).unwrap();
|
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
|
let f = c
|
||||||
.found
|
.found
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.origin == Origin::Xfs)
|
.find(|f| f.origin == Origin::Xfs)
|
||||||
.unwrap_or_else(|| panic!("nada do XFS: {:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::<Vec<_>>()));
|
.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"nada do XFS: {:?}",
|
||||||
|
c.found
|
||||||
|
.iter()
|
||||||
|
.map(|f| (&f.name, f.origin))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
assert_eq!(f.name, "relatorio.dat");
|
assert_eq!(f.name, "relatorio.dat");
|
||||||
assert_eq!(f.dir.as_deref(), Some("/docs"));
|
assert_eq!(f.dir.as_deref(), Some("/docs"));
|
||||||
assert!(f.confidence >= Confidence::Medium, "{:?}", f.note);
|
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!(got.starts_with(&doc), "conteúdo inicial difere");
|
||||||
assert_eq!(got.len() % 4096, 0);
|
assert_eq!(got.len() % 4096, 0);
|
||||||
// Só um arquivo apagado.
|
// 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]
|
#[test]
|
||||||
@@ -178,7 +258,13 @@ fn reference_image_is_recognized() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
init();
|
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();
|
let l = c.layout.unwrap();
|
||||||
assert_eq!(l.partitions[0].fs, FsKind::Xfs);
|
assert_eq!(l.partitions[0].fs, FsKind::Xfs);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user