Compare commits
No files matched your search
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-10-19
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- **Sistemas de arquivos com metadados:** ext4 (inodes apagados e cópias antigas no diário), HFS+ (sobras de nós e diário), Btrfs (versões antigas das árvores; extensões zlib, zstd e lzo), XFS (inodes liberados e nomes de entradas liberadas) e APFS (instantâneos e nós antigos). ReFS ainda é só detectado.
|
||||||
|
- **Volumes cifrados:** BitLocker (senha, chave de recuperação, .BEK, chave limpa), LUKS1/LUKS2 e FileVault 2 (Core Storage) abrem com a senha ou a chave e são varridos por dentro. As senhas nunca vão para o arquivo de sessão.
|
||||||
|
- **RAID e LVM:** RAID 0, 1, 4, 5, 6, 10 e linear a partir de superblocos do mdadm, inclusive degradado; descoberta automática de nível, faixa, layout e ordem quando não há metadados; LVM2 (linear, em faixas, espelhado e RAID).
|
||||||
|
- **Imagens forenses:** leitura de E01/Ex01 e AFF4; gravação de E01 e AFF4; comando `guara verify`.
|
||||||
|
- **Carving:** JPEG fragmentado remontado (até três fragmentos), reconstrução do índice de MP4/MOV sem moov (H.264/HEVC, com vídeo de referência) e 24 formatos reconhecidos pela estrutura.
|
||||||
|
- **Interface:** campos de senha, RAID, vídeo de referência e formato da imagem.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
- Abrir de novo uma sessão recente já aberta não cria mais cópias iguais na aba de sessões.
|
||||||
|
|
||||||
|
### Limitações
|
||||||
|
- Storage Spaces e ReFS (metadados) não suportados; XFS e APFS testados com exclusões simuladas; áudio não é reconstruído em MP4 sem moov.
|
||||||
|
|
||||||
## [0.6.0] - 2026-10-18
|
## [0.6.0] - 2026-10-18
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -58,6 +69,27 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"cpufeatures",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ascii"
|
name = "ascii"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -70,12 +102,27 @@ version = "1.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.2"
|
version = "2.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -94,18 +141,45 @@ dependencies = [
|
|||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.25.2"
|
version = "1.25.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder-lite"
|
name = "byteorder-lite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
|
||||||
|
dependencies = [
|
||||||
|
"libbz2-rs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -124,6 +198,16 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -155,7 +239,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -194,6 +278,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -215,6 +305,17 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -223,6 +324,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -237,6 +339,12 @@ dependencies = [
|
|||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -317,42 +425,69 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "guara-cli"
|
name = "guara-cli"
|
||||||
version = "0.6.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"guara-core",
|
"guara-core",
|
||||||
|
"rpassword",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tiny_http",
|
"tiny_http",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "guara-core"
|
name = "guara-core"
|
||||||
version = "0.6.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"argon2",
|
||||||
|
"bzip2",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
"hmac",
|
||||||
"image",
|
"image",
|
||||||
"jpeg-decoder",
|
"jpeg-decoder",
|
||||||
"jpeg-encoder",
|
"jpeg-encoder",
|
||||||
"libc",
|
"libc",
|
||||||
|
"lz4_flex",
|
||||||
"md-5",
|
"md-5",
|
||||||
|
"mp4",
|
||||||
|
"pbkdf2",
|
||||||
|
"ripemd",
|
||||||
|
"ruzstd",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"snap",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror 2.0.21",
|
||||||
"tiny_http",
|
"tiny_http",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpdate"
|
name = "httpdate"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -385,6 +520,25 @@ dependencies = [
|
|||||||
"quick-error",
|
"quick-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -409,6 +563,12 @@ version = "0.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b454d911ac55068f53495488d8ccd0646eaa540c033a28ee15b07838afafb01f"
|
checksum = "b454d911ac55068f53495488d8ccd0646eaa540c033a28ee15b07838afafb01f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libbz2-rs-sys"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.191"
|
version = "0.2.191"
|
||||||
@@ -427,6 +587,12 @@ version = "0.4.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lz4_flex"
|
||||||
|
version = "0.11.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -473,6 +639,20 @@ dependencies = [
|
|||||||
"pxfm",
|
"pxfm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mp4"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"num-rational",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.31.3"
|
version = "0.31.3"
|
||||||
@@ -485,6 +665,37 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-rational"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -521,6 +732,27 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -570,6 +802,42 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ripemd"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rpassword"
|
||||||
|
version = "7.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rtoolbox",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rtoolbox"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.5"
|
version = "1.1.5"
|
||||||
@@ -583,6 +851,15 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruzstd"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f"
|
||||||
|
dependencies = [
|
||||||
|
"twox-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -610,7 +887,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -654,12 +931,41 @@ version = "0.3.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snap"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "199905e6153d6405f9728fe44daace35f8f837bbf830bb6e85fbd5828709a886"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.9"
|
version = "3.0.9"
|
||||||
@@ -684,13 +990,33 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.21"
|
version = "2.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -701,7 +1027,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -716,6 +1042,16 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "twox-hash"
|
||||||
|
version = "1.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.1"
|
version = "1.20.1"
|
||||||
@@ -840,6 +1176,21 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"flate2",
|
||||||
|
"indexmap",
|
||||||
|
"memchr",
|
||||||
|
"zopfli",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlib-rs"
|
name = "zlib-rs"
|
||||||
version = "0.6.8"
|
version = "0.6.8"
|
||||||
@@ -851,3 +1202,15 @@ name = "zmij"
|
|||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"crc32fast",
|
||||||
|
"log",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
@@ -3,7 +3,7 @@ resolver = "2"
|
|||||||
members = ["crates/guara-core", "crates/guara-cli"]
|
members = ["crates/guara-core", "crates/guara-cli"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.6.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/lucasrguerra/guara"
|
repository = "https://github.com/lucasrguerra/guara"
|
||||||
@@ -16,3 +16,7 @@ codegen-units = 1
|
|||||||
opt-level = 3
|
opt-level = 3
|
||||||
strip = true
|
strip = true
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
|
# Dependências otimizadas também em debug: os testes de cifra fazem milhões de SHA-256.
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
@@ -118,6 +118,10 @@ Veja [CONTRIBUTING.md](CONTRIBUTING.md). Versões seguem [SemVer](https://semver
|
|||||||
|
|
||||||
[MIT](LICENSE-MIT) ou [Apache-2.0](LICENSE-APACHE), à sua escolha.
|
[MIT](LICENSE-MIT) ou [Apache-2.0](LICENSE-APACHE), à sua escolha.
|
||||||
|
|
||||||
|
## Novidades da 1.0.0
|
||||||
|
|
||||||
|
Metadados de ext4, HFS+, Btrfs, XFS e APFS; abertura de volumes BitLocker, LUKS e FileVault 2 com senha ou chave; RAID (0/1/4/5/6/10) e LVM remontados, com parâmetros descobertos sozinhos; imagens E01, Ex01 e AFF4; JPEG fragmentado e MP4 sem índice. Detalhes e limitações no [CHANGELOG](CHANGELOG.md).
|
||||||
|
|
||||||
## Novidades da 0.6.0
|
## Novidades da 0.6.0
|
||||||
|
|
||||||
- Saúde do disco completa, no estilo do CrystalDiskInfo: estado com % de vida útil, temperatura, horas e vezes ligado, total lido e gravado, ficha do dispositivo (modelo, firmware, interface, modo de transferência, rotação, recursos) e a tabela inteira de atributos SMART (ATA e NVMe).
|
- Saúde do disco completa, no estilo do CrystalDiskInfo: estado com % de vida útil, temperatura, horas e vezes ligado, total lido e gravado, ficha do dispositivo (modelo, firmware, interface, modo de transferência, rotação, recursos) e a tabela inteira de atributos SMART (ATA e NVMe).
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "guara-gui"
|
name = "guara-gui"
|
||||||
description = "Guará: interface gráfica de recuperação de arquivos"
|
description = "Guará: interface gráfica de recuperação de arquivos"
|
||||||
version = "0.6.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/lucasrguerra/guara"
|
repository = "https://github.com/lucasrguerra/guara"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
|
After Width: | Height: | Size: 835 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Guara",
|
"productName": "Guara",
|
||||||
"version": "0.6.0",
|
"version": "1.0.0",
|
||||||
"identifier": "dev.guara.app",
|
"identifier": "dev.guara.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../ui"
|
"frontendDist": "../ui"
|
||||||
|
|||||||
@@ -215,8 +215,11 @@ async function openRecent(r) {
|
|||||||
try {
|
try {
|
||||||
const { job } = await rpc('session_load', { path: r.path });
|
const { job } = await rpc('session_load', { path: r.path });
|
||||||
const st = await rpc('status', { job });
|
const st = await rpc('status', { job });
|
||||||
const s = { job, target: st.target, label: 'Sessão: ' + st.target.split(/[\\/]/).pop() };
|
let s = S.sessions.find((x) => x.job === job);
|
||||||
S.sessions.unshift(s);
|
if (!s) {
|
||||||
|
s = { job, target: st.target, label: 'Sessão: ' + st.target.split(/[\\/]/).pop() };
|
||||||
|
S.sessions.unshift(s);
|
||||||
|
}
|
||||||
openJob(s);
|
openJob(s);
|
||||||
} catch (e) { info({ title: 'Não foi possível abrir a sessão', html: `<p>${esc(e.message)}</p>` }); }
|
} catch (e) { info({ title: 'Não foi possível abrir a sessão', html: `<p>${esc(e.message)}</p>` }); }
|
||||||
}
|
}
|
||||||
@@ -246,8 +249,11 @@ $('#loadSessionBtn').onclick = async () => {
|
|||||||
try {
|
try {
|
||||||
const { job } = await rpc('session_load', { path });
|
const { job } = await rpc('session_load', { path });
|
||||||
const st = await rpc('status', { job });
|
const st = await rpc('status', { job });
|
||||||
const s = { job, target: st.target, label: 'Sessão: ' + st.target.split(/[\\/]/).pop() };
|
let s = S.sessions.find((x) => x.job === job);
|
||||||
S.sessions.unshift(s);
|
if (!s) {
|
||||||
|
s = { job, target: st.target, label: 'Sessão: ' + st.target.split(/[\\/]/).pop() };
|
||||||
|
S.sessions.unshift(s);
|
||||||
|
}
|
||||||
openJob(s);
|
openJob(s);
|
||||||
} catch (e) { info({ title: 'Não foi possível abrir a sessão', html: `<p>${esc(e.message)}</p>` }); }
|
} catch (e) { info({ title: 'Não foi possível abrir a sessão', html: `<p>${esc(e.message)}</p>` }); }
|
||||||
};
|
};
|
||||||
@@ -310,7 +316,7 @@ function paintHealth() {
|
|||||||
tile('Total gravado', h.host_writes == null ? null : human(h.host_writes)),
|
tile('Total gravado', h.host_writes == null ? null : human(h.host_writes)),
|
||||||
].join('');
|
].join('');
|
||||||
const info = (h.info || []).map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join('');
|
const info = (h.info || []).map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join('');
|
||||||
const dash = (x) => x == null ? '<span class="dim">–</span>' : x;
|
const dash = (x) => x == null ? '<span class="dim">-</span>' : x;
|
||||||
const attrs = h.attributes || [];
|
const attrs = h.attributes || [];
|
||||||
const table = attrs.length ? `<details class="adv h-attrs"${bad ? ' open' : ''}><summary>Atributos SMART (${attrs.length})</summary>
|
const table = attrs.length ? `<details class="adv h-attrs"${bad ? ' open' : ''}><summary>Atributos SMART (${attrs.length})</summary>
|
||||||
<div class="h-scroll"><table class="h-table"><thead><tr><th></th><th>ID</th><th>Atributo</th><th>Atual</th><th>Pior</th><th>Limite</th><th>Bruto</th></tr></thead><tbody>
|
<div class="h-scroll"><table class="h-table"><thead><tr><th></th><th>ID</th><th>Atributo</th><th>Atual</th><th>Pior</th><th>Limite</th><th>Bruto</th></tr></thead><tbody>
|
||||||
@@ -403,10 +409,33 @@ function renderDevice(err) {
|
|||||||
<div class="row" id="optCats">${CATS.map(([k, n]) => `<label class="check"><input type="checkbox" value="${k}"> ${n}</label>`).join('')}</div>
|
<div class="row" id="optCats">${CATS.map(([k, n]) => `<label class="check"><input type="checkbox" value="${k}"> ${n}</label>`).join('')}</div>
|
||||||
<div class="sub">Sem nenhuma categoria marcada, todos os tipos são procurados.</div>
|
<div class="sub">Sem nenhuma categoria marcada, todos os tipos são procurados.</div>
|
||||||
</details>
|
</details>
|
||||||
|
<details class="adv"><summary>Volumes cifrados, RAID e vídeos</summary>
|
||||||
|
<label class="fld"><span>Senha do volume (BitLocker, LUKS, FileVault 2)</span><input class="input" id="optPw" type="password" autocomplete="off"></label>
|
||||||
|
<label class="fld"><span>Chave de recuperação do BitLocker</span><input class="input" id="optRk" placeholder="000000-000000-... (8 grupos)" autocomplete="off"></label>
|
||||||
|
<label class="fld"><span>Arquivo de chave (.BEK ou keyfile)</span><input class="input" id="optKf" placeholder="/caminho/chave.BEK"></label>
|
||||||
|
<label class="fld"><span>Outros discos do mesmo conjunto (RAID/LVM), um caminho por linha</span><textarea class="input" id="optMembers" rows="3"></textarea></label>
|
||||||
|
<label class="check"><input type="checkbox" id="optRaid"><span><b>Estes discos formam um RAID sem metadados</b><br><span class="sub">O Guará descobre sozinho o nível, a faixa, o layout e a ordem dos discos.</span></span></label>
|
||||||
|
<label class="fld"><span>Vídeo íntegro da mesma câmera/drone (refaz o índice de MP4 sem moov)</span><input class="input" id="optRef" placeholder="/caminho/video-bom.mp4"></label>
|
||||||
|
<label class="check"><input type="checkbox" id="optRemember"><span><b>Lembrar as chaves dos volumes abertos na sessão</b><br><span class="sub">A senha nunca é gravada; só a chave derivada.</span></span></label>
|
||||||
|
</details>
|
||||||
</div></div>`;
|
</div></div>`;
|
||||||
$('#elevate')?.addEventListener('click', () => tauri.core.invoke('relaunch_elevated').catch((e) => toast(String(e))));
|
$('#elevate')?.addEventListener('click', () => tauri.core.invoke('relaunch_elevated').catch((e) => toast(String(e))));
|
||||||
const opts = (mode) => ({
|
const opts = (mode) => ({
|
||||||
mode,
|
mode,
|
||||||
|
assembly: (() => {
|
||||||
|
const v = (id) => ($(id)?.value || '').trim();
|
||||||
|
const secrets = [];
|
||||||
|
if (v('#optPw')) secrets.push({ kind: 'password', value: v('#optPw') });
|
||||||
|
if (v('#optRk')) secrets.push({ kind: 'recovery', value: v('#optRk') });
|
||||||
|
if (v('#optKf')) secrets.push({ kind: 'keyfile', path: v('#optKf') });
|
||||||
|
return {
|
||||||
|
secrets,
|
||||||
|
members: v('#optMembers').split('\n').map((x) => x.trim()).filter(Boolean),
|
||||||
|
raid: $('#optRaid')?.checked ? {} : null,
|
||||||
|
remember_keys: $('#optRemember')?.checked || false,
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
references: $('#optRef')?.value.trim() ? [$('#optRef').value.trim()] : [],
|
||||||
include_allocated: $('#optAlloc')?.checked || false,
|
include_allocated: $('#optAlloc')?.checked || false,
|
||||||
categories: [...document.querySelectorAll('#optCats input:checked')].map((i) => i.value),
|
categories: [...document.querySelectorAll('#optCats input:checked')].map((i) => i.value),
|
||||||
});
|
});
|
||||||
@@ -436,10 +465,18 @@ async function startScan(options) {
|
|||||||
|
|
||||||
async function startImage() {
|
async function startImage() {
|
||||||
const base = (S.defaults.home || '') + (S.defaults.sep || '/');
|
const base = (S.defaults.home || '') + (S.defaults.sep || '/');
|
||||||
const dest = await ask({ title: 'Criar imagem do disco', text: 'Onde salvar o arquivo de imagem? Use <b>outro disco</b> com espaço suficiente e um nome que ainda não exista.', value: base + 'disco-guara.img', ok: 'Criar imagem' });
|
const f = await askForm({ title: 'Criar imagem do disco', text: 'Onde salvar o arquivo de imagem? Use <b>outro disco</b> com espaço suficiente e um nome que ainda não exista.', ok: 'Criar imagem', fields: [
|
||||||
if (!dest) return;
|
{ name: 'dest', label: 'Arquivo de destino', value: base + 'disco-guara.img' },
|
||||||
|
{ name: 'format', label: 'Formato', value: 'raw', options: [['raw', 'Bruta (.img)'], ['e01', 'Expert Witness (.E01, com hash e metadados)'], ['aff4', 'AFF4 (.aff4)']] },
|
||||||
|
{ name: 'case', label: 'Número do caso (E01/AFF4)', value: '' },
|
||||||
|
{ name: 'examiner', label: 'Examinador (E01/AFF4)', value: '' },
|
||||||
|
] });
|
||||||
|
if (!f || !f.dest) return;
|
||||||
|
const dest = f.dest;
|
||||||
|
const format = f.format === 'e01' ? { kind: 'e01', case_number: f.case, examiner: f.examiner }
|
||||||
|
: f.format === 'aff4' ? { kind: 'aff4', case_number: f.case, examiner: f.examiner } : { kind: 'raw' };
|
||||||
try {
|
try {
|
||||||
const { job } = await rpc('image_start', { path: S.device.path, dest });
|
const { job } = await rpc('image_start', { path: S.device.path, dest, format });
|
||||||
watchAux(job, 'Criando imagem do disco', (st) => {
|
watchAux(job, 'Criando imagem do disco', (st) => {
|
||||||
const r = st.report || {};
|
const r = st.report || {};
|
||||||
const bad = (r.bad_ranges || []).length;
|
const bad = (r.bad_ranges || []).length;
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ serde_json = "1"
|
|||||||
tiny_http = "0.12"
|
tiny_http = "0.12"
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
ctrlc = "3"
|
ctrlc = "3"
|
||||||
|
rpassword = "7"
|
||||||
@@ -31,6 +31,8 @@ enum Cmd {
|
|||||||
path: String,
|
path: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
#[command(flatten)]
|
||||||
|
asm: AsmArgs,
|
||||||
},
|
},
|
||||||
/// Saúde do disco (SMART): estado, temperatura, uso e tabela de atributos
|
/// Saúde do disco (SMART): estado, temperatura, uso e tabela de atributos
|
||||||
Health {
|
Health {
|
||||||
@@ -56,7 +58,36 @@ enum Cmd {
|
|||||||
filter: FilterArgs,
|
filter: FilterArgs,
|
||||||
},
|
},
|
||||||
/// Cria uma imagem do disco (tolerante a setores ruins) para trabalhar na cópia
|
/// Cria uma imagem do disco (tolerante a setores ruins) para trabalhar na cópia
|
||||||
Image { path: String, dest: String },
|
Image {
|
||||||
|
path: String,
|
||||||
|
dest: String,
|
||||||
|
/// Formato da imagem: raw (padrão), e01 (Expert Witness) ou aff4 (AFF4 v1.0), com hash e metadados
|
||||||
|
#[arg(long, default_value = "raw")]
|
||||||
|
format: String,
|
||||||
|
/// E01: número do caso
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
case: String,
|
||||||
|
/// E01: examinador
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
examiner: String,
|
||||||
|
/// E01: descrição da evidência
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
description: String,
|
||||||
|
/// E01: número da evidência
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
evidence: String,
|
||||||
|
/// E01: observações
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
notes: String,
|
||||||
|
/// E01: tamanho máximo de cada segmento em MiB (0 = um arquivo só)
|
||||||
|
#[arg(long, default_value_t = 1500)]
|
||||||
|
segment_mib: u64,
|
||||||
|
/// E01: compressão 0 (nenhuma) a 9
|
||||||
|
#[arg(long, default_value_t = 1)]
|
||||||
|
compression: u32,
|
||||||
|
},
|
||||||
|
/// Confere uma imagem E01/Ex01: recalcula o MD5 e compara com o gravado na própria imagem
|
||||||
|
Verify { path: String },
|
||||||
/// Exporta os achados de uma sessão para CSV (abre no Excel)
|
/// Exporta os achados de uma sessão para CSV (abre no Excel)
|
||||||
Export {
|
Export {
|
||||||
session: String,
|
session: String,
|
||||||
@@ -140,6 +171,120 @@ struct OutArgs {
|
|||||||
json: bool,
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Montagem do disco: volumes cifrados, RAID e LVM.
|
||||||
|
#[derive(clap::Args, Clone, Default)]
|
||||||
|
struct AsmArgs {
|
||||||
|
/// Senha de volume cifrado (BitLocker, LUKS, FileVault 2). Prefira --ask-password ou GUARA_PASSWORD
|
||||||
|
#[arg(long = "password")]
|
||||||
|
passwords: Vec<String>,
|
||||||
|
/// Pergunta a senha no terminal, sem mostrar o que é digitado
|
||||||
|
#[arg(long)]
|
||||||
|
ask_password: bool,
|
||||||
|
/// Chave de recuperação numérica do BitLocker (8 grupos de 6 dígitos)
|
||||||
|
#[arg(long = "recovery-key")]
|
||||||
|
recovery_keys: Vec<String>,
|
||||||
|
/// Arquivo de chave (.BEK do BitLocker ou keyfile do LUKS)
|
||||||
|
#[arg(long = "key-file")]
|
||||||
|
key_files: Vec<String>,
|
||||||
|
/// Chave do volume em hexadecimal (a que o Guará mostra ao abrir um volume)
|
||||||
|
#[arg(long = "volume-key")]
|
||||||
|
volume_keys: Vec<String>,
|
||||||
|
/// Outro disco ou imagem do mesmo conjunto (RAID/LVM). Pode repetir; "-" marca um disco ausente
|
||||||
|
#[arg(long = "member")]
|
||||||
|
members: Vec<String>,
|
||||||
|
/// Os discos (alvo + --member, nesta ordem) formam um RAID sem metadados: descobre sozinho nível, faixa e layout
|
||||||
|
#[arg(long)]
|
||||||
|
raid: bool,
|
||||||
|
/// Fixa o nível do RAID: 0, 5, 6, 1
|
||||||
|
#[arg(long)]
|
||||||
|
raid_level: Option<String>,
|
||||||
|
/// Fixa o tamanho da faixa do RAID em KiB
|
||||||
|
#[arg(long)]
|
||||||
|
raid_chunk_kb: Option<u64>,
|
||||||
|
/// Fixa o layout: left-symmetric, left-asymmetric, right-symmetric, right-asymmetric
|
||||||
|
#[arg(long)]
|
||||||
|
raid_layout: Option<String>,
|
||||||
|
/// Ordem dos discos no RAID (índices; 0 = alvo)
|
||||||
|
#[arg(long, value_delimiter = ',')]
|
||||||
|
raid_order: Vec<usize>,
|
||||||
|
/// Guarda na sessão as chaves dos volumes abertos (a senha nunca é guardada)
|
||||||
|
#[arg(long)]
|
||||||
|
remember_keys: bool,
|
||||||
|
/// Vídeo íntegro da mesma câmera/drone para refazer o índice de MP4 sem moov (pode repetir)
|
||||||
|
#[arg(long = "reference")]
|
||||||
|
references: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsmArgs {
|
||||||
|
fn assembly(&self) -> Result<guara_core::assemble::Assembly, String> {
|
||||||
|
use guara_core::assemble::{Assembly, RaidSpec, SecretInput};
|
||||||
|
let mut secrets = vec![];
|
||||||
|
for p in &self.passwords {
|
||||||
|
secrets.push(SecretInput::Password { value: p.clone() });
|
||||||
|
}
|
||||||
|
if let Ok(p) = std::env::var("GUARA_PASSWORD") {
|
||||||
|
if !p.is_empty() {
|
||||||
|
secrets.push(SecretInput::Password { value: p });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.ask_password {
|
||||||
|
let p = rpassword::prompt_password("Senha do volume: ").map_err(s)?;
|
||||||
|
secrets.push(SecretInput::Password { value: p });
|
||||||
|
}
|
||||||
|
for r in &self.recovery_keys {
|
||||||
|
secrets.push(SecretInput::Recovery { value: r.clone() });
|
||||||
|
}
|
||||||
|
for k in &self.key_files {
|
||||||
|
secrets.push(SecretInput::Keyfile { path: k.clone() });
|
||||||
|
}
|
||||||
|
for k in &self.volume_keys {
|
||||||
|
secrets.push(SecretInput::Key { hex: k.clone() });
|
||||||
|
}
|
||||||
|
let raid = if self.raid
|
||||||
|
|| self.raid_level.is_some()
|
||||||
|
|| self.raid_chunk_kb.is_some()
|
||||||
|
|| self.raid_layout.is_some()
|
||||||
|
|| !self.raid_order.is_empty()
|
||||||
|
{
|
||||||
|
let level = match self.raid_level.as_deref() {
|
||||||
|
None => None,
|
||||||
|
Some("0") => Some(guara_core::raid::Level::R0),
|
||||||
|
Some("1") => Some(guara_core::raid::Level::R1),
|
||||||
|
Some("5") => Some(guara_core::raid::Level::R5),
|
||||||
|
Some("6") => Some(guara_core::raid::Level::R6),
|
||||||
|
Some(x) => {
|
||||||
|
return Err(format!(
|
||||||
|
"nível de RAID não suportado: {x} (use 0, 1, 5 ou 6)"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let layout = match self.raid_layout.as_deref() {
|
||||||
|
None => None,
|
||||||
|
Some("left-symmetric") => Some(guara_core::raid::Layout::LeftSymmetric),
|
||||||
|
Some("left-asymmetric") => Some(guara_core::raid::Layout::LeftAsymmetric),
|
||||||
|
Some("right-symmetric") => Some(guara_core::raid::Layout::RightSymmetric),
|
||||||
|
Some("right-asymmetric") => Some(guara_core::raid::Layout::RightAsymmetric),
|
||||||
|
Some(x) => return Err(format!("layout desconhecido: {x}")),
|
||||||
|
};
|
||||||
|
Some(RaidSpec {
|
||||||
|
level,
|
||||||
|
chunk_kb: self.raid_chunk_kb,
|
||||||
|
layout,
|
||||||
|
order: (!self.raid_order.is_empty()).then(|| self.raid_order.clone()),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(Assembly {
|
||||||
|
raid,
|
||||||
|
members: self.members.iter().filter(|m| *m != "-").cloned().collect(),
|
||||||
|
secrets,
|
||||||
|
remember_keys: self.remember_keys,
|
||||||
|
keys: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(clap::Args)]
|
#[derive(clap::Args)]
|
||||||
struct ScanArgs {
|
struct ScanArgs {
|
||||||
/// Dispositivo ou imagem
|
/// Dispositivo ou imagem
|
||||||
@@ -154,6 +299,8 @@ struct ScanArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
recover: Option<String>,
|
recover: Option<String>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
|
asm: AsmArgs,
|
||||||
|
#[command(flatten)]
|
||||||
filter: FilterArgs,
|
filter: FilterArgs,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
out: OutArgs,
|
out: OutArgs,
|
||||||
@@ -204,8 +351,14 @@ fn run(cli: Cli) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Cmd::Inspect { path, json } => {
|
Cmd::Inspect { path, json, asm } => {
|
||||||
let v = eng.rpc("inspect", json!({ "path": path })).map_err(s)?;
|
let a = asm.assembly()?;
|
||||||
|
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 {
|
||||||
@@ -231,6 +384,8 @@ fn run(cli: Cli) -> Result<(), String> {
|
|||||||
},
|
},
|
||||||
include_allocated: a.include_allocated,
|
include_allocated: a.include_allocated,
|
||||||
categories: parse_cats(&a.filter.category)?,
|
categories: parse_cats(&a.filter.category)?,
|
||||||
|
assembly: a.asm.assembly()?,
|
||||||
|
references: a.asm.references.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let job = eng.start_scan(&a.path, opts).map_err(s)?;
|
let job = eng.start_scan(&a.path, opts).map_err(s)?;
|
||||||
@@ -272,8 +427,88 @@ fn run(cli: Cli) -> Result<(), String> {
|
|||||||
eng.save_session(&job, &session).map_err(s)?;
|
eng.save_session(&job, &session).map_err(s)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Cmd::Image { path, dest } => {
|
Cmd::Verify { path } if path.to_ascii_lowercase().ends_with(".aff4") => {
|
||||||
let job = eng.start_image(&path, &dest).map_err(s)?;
|
let a = guara_core::aff4::Aff4Source::open(std::path::Path::new(&path)).map_err(s)?;
|
||||||
|
println!("Formato: AFF4");
|
||||||
|
for (k, v) in &a.info {
|
||||||
|
println!("{k}: {v}");
|
||||||
|
}
|
||||||
|
eprintln!("Recalculando o hash...");
|
||||||
|
match a.verify_stream_md5().map_err(s)? {
|
||||||
|
Some(true) => {
|
||||||
|
println!("MD5 gravado e recalculado conferem.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Some(false) => Err("O MD5 recalculado difere do gravado na imagem.".into()),
|
||||||
|
None => {
|
||||||
|
println!("Esta imagem não traz MD5 para conferir.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cmd::Verify { path } => {
|
||||||
|
let e = guara_core::ewf::EwfSource::open(std::path::Path::new(&path)).map_err(s)?;
|
||||||
|
println!(
|
||||||
|
"Formato: {}",
|
||||||
|
if e.info.version == 2 {
|
||||||
|
"Ex01 (EnCase 7)"
|
||||||
|
} else {
|
||||||
|
"E01 (EnCase)"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
println!("Segmentos: {}", e.info.segments);
|
||||||
|
for (k, v) in &e.info.fields {
|
||||||
|
println!("{k}: {v}");
|
||||||
|
}
|
||||||
|
eprintln!("Recalculando o hash...");
|
||||||
|
match e.verify().map_err(s)? {
|
||||||
|
Some(true) => {
|
||||||
|
println!("MD5 gravado e recalculado conferem: {}", e.info.md5.clone().unwrap_or_default());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Some(false) => Err("O MD5 recalculado difere do gravado na imagem: ela está corrompida ou foi alterada.".into()),
|
||||||
|
None => {
|
||||||
|
println!("Esta imagem não traz hash gravado para conferir.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cmd::Image {
|
||||||
|
path,
|
||||||
|
dest,
|
||||||
|
format,
|
||||||
|
case,
|
||||||
|
examiner,
|
||||||
|
description,
|
||||||
|
evidence,
|
||||||
|
notes,
|
||||||
|
segment_mib,
|
||||||
|
compression,
|
||||||
|
} => {
|
||||||
|
let fmt = match format.as_str() {
|
||||||
|
"raw" => guara_core::image::ImageFormat::Raw,
|
||||||
|
"e01" => guara_core::image::ImageFormat::E01 {
|
||||||
|
case_number: case,
|
||||||
|
description,
|
||||||
|
evidence_number: evidence,
|
||||||
|
examiner,
|
||||||
|
notes,
|
||||||
|
segment_mib,
|
||||||
|
compression,
|
||||||
|
},
|
||||||
|
"aff4" => guara_core::image::ImageFormat::Aff4 {
|
||||||
|
case_number: case,
|
||||||
|
examiner,
|
||||||
|
evidence_number: evidence,
|
||||||
|
notes,
|
||||||
|
},
|
||||||
|
x => {
|
||||||
|
return Err(format!(
|
||||||
|
"formato de imagem desconhecido: {x} (use raw, e01 ou aff4)"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let job = eng.start_image_as(&path, &dest, &fmt).map_err(s)?;
|
||||||
let st = wait(&eng, &job, None)?;
|
let st = wait(&eng, &job, None)?;
|
||||||
if st["status"] == "error" {
|
if st["status"] == "error" {
|
||||||
return Err(st["error"].as_str().unwrap_or("falha").to_string());
|
return Err(st["error"].as_str().unwrap_or("falha").to_string());
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ image = { version = "0.25", default-features = false, features = ["png", "gif",
|
|||||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tiny_http = "0.12"
|
tiny_http = "0.12"
|
||||||
|
bzip2 = "0.6"
|
||||||
|
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||||
|
snap = "1"
|
||||||
|
ruzstd = "0.7"
|
||||||
|
lz4_flex = { version = "0.11", default-features = false, features = ["safe-decode", "safe-encode"] }
|
||||||
|
aes = "0.8"
|
||||||
|
hmac = "0.12"
|
||||||
|
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
|
||||||
|
ripemd = "0.1"
|
||||||
|
argon2 = "0.5"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
@@ -28,3 +38,4 @@ windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Storage
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
mp4 = "0.14"
|
||||||
@@ -0,0 +1,846 @@
|
|||||||
|
//! AFF4 (Advanced Forensic Format 4, padrão v1.0): contêiner ZIP com metadados em Turtle.
|
||||||
|
//!
|
||||||
|
//! Leitura de imagens lineares e mapeadas (inclusive esparsas), com blocos comprimidos por
|
||||||
|
//! snappy, deflate, LZ4 ou sem compressão. Gravação de uma imagem linear com blocos snappy.
|
||||||
|
//! Conjuntos em vários volumes (spanned/striped) não são suportados.
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::hashing::MultiHasher;
|
||||||
|
use crate::image::ImageReport;
|
||||||
|
use crate::source::{read_exact_at, Source};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
pub fn is_aff4(head: &[u8]) -> bool {
|
||||||
|
// ZIP local file header + primeiro nome "container.description" ou "version.txt".
|
||||||
|
head.len() > 50
|
||||||
|
&& head.starts_with(b"PK\x03\x04")
|
||||||
|
&& (find(&head[30..head.len().min(80)], b"container.description")
|
||||||
|
|| find(&head[30..head.len().min(80)], b"version.txt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find(h: &[u8], n: &[u8]) -> bool {
|
||||||
|
h.windows(n.len()).any(|w| w == n)
|
||||||
|
}
|
||||||
|
|
||||||
|
const NS: &str = "http://aff4.org/Schema#";
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ Turtle
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
struct Resource {
|
||||||
|
types: Vec<String>,
|
||||||
|
props: HashMap<String, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resource {
|
||||||
|
fn get(&self, k: &str) -> Option<&str> {
|
||||||
|
self.props
|
||||||
|
.get(k)
|
||||||
|
.and_then(|v| v.first())
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
}
|
||||||
|
fn num(&self, k: &str) -> Option<u64> {
|
||||||
|
self.get(k)?.parse().ok()
|
||||||
|
}
|
||||||
|
fn is(&self, t: &str) -> bool {
|
||||||
|
self.types.iter().any(|x| x == t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Turtle<'a> {
|
||||||
|
b: &'a [u8],
|
||||||
|
i: usize,
|
||||||
|
prefixes: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Turtle<'_> {
|
||||||
|
fn ws(&mut self) {
|
||||||
|
loop {
|
||||||
|
while self.i < self.b.len() && self.b[self.i].is_ascii_whitespace() {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
if self.i < self.b.len() && self.b[self.i] == b'#' {
|
||||||
|
while self.i < self.b.len() && self.b[self.i] != b'\n' {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn expand(&self, tok: &str) -> String {
|
||||||
|
if let Some((p, l)) = tok.split_once(':') {
|
||||||
|
if let Some(base) = self.prefixes.get(p) {
|
||||||
|
return format!("{base}{l}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tok.to_string()
|
||||||
|
}
|
||||||
|
/// Um termo: <iri>, "literal"[^^tipo], nome:prefixado ou `a`.
|
||||||
|
fn term(&mut self) -> Option<String> {
|
||||||
|
self.ws();
|
||||||
|
let c = *self.b.get(self.i)?;
|
||||||
|
match c {
|
||||||
|
b'<' => {
|
||||||
|
let s = self.i + 1;
|
||||||
|
while self.i < self.b.len() && self.b[self.i] != b'>' {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
let v = String::from_utf8_lossy(&self.b[s..self.i]).to_string();
|
||||||
|
self.i += 1;
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
b'"' => {
|
||||||
|
self.i += 1;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
while self.i < self.b.len() && self.b[self.i] != b'"' {
|
||||||
|
if self.b[self.i] == b'\\' && self.i + 1 < self.b.len() {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
out.push(self.b[self.i]);
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
self.i += 1;
|
||||||
|
if self.b[self.i..].starts_with(b"^^") {
|
||||||
|
self.i += 2;
|
||||||
|
let _ = self.term();
|
||||||
|
} else if self.b.get(self.i) == Some(&b'@') {
|
||||||
|
while self.i < self.b.len() && !self.b[self.i].is_ascii_whitespace() {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(String::from_utf8_lossy(&out).to_string())
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let s = self.i;
|
||||||
|
while self.i < self.b.len()
|
||||||
|
&& !self.b[self.i].is_ascii_whitespace()
|
||||||
|
&& !matches!(self.b[self.i], b';' | b',')
|
||||||
|
{
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
// ponto final de sentença colado ao termo
|
||||||
|
let mut e = self.i;
|
||||||
|
if e > s && self.b[e - 1] == b'.' {
|
||||||
|
e -= 1;
|
||||||
|
self.i = e;
|
||||||
|
}
|
||||||
|
let tok = String::from_utf8_lossy(&self.b[s..e]).to_string();
|
||||||
|
Some(if tok == "a" { tok } else { self.expand(&tok) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_turtle(text: &str) -> HashMap<String, Resource> {
|
||||||
|
let mut t = Turtle {
|
||||||
|
b: text.as_bytes(),
|
||||||
|
i: 0,
|
||||||
|
prefixes: HashMap::new(),
|
||||||
|
};
|
||||||
|
let mut out: HashMap<String, Resource> = HashMap::new();
|
||||||
|
loop {
|
||||||
|
t.ws();
|
||||||
|
if t.i >= t.b.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if t.b[t.i..].starts_with(b"@prefix") {
|
||||||
|
t.i += 7;
|
||||||
|
t.ws();
|
||||||
|
let s = t.i;
|
||||||
|
while t.i < t.b.len() && t.b[t.i] != b':' {
|
||||||
|
t.i += 1;
|
||||||
|
}
|
||||||
|
let name = String::from_utf8_lossy(&t.b[s..t.i]).trim().to_string();
|
||||||
|
t.i += 1;
|
||||||
|
let iri = t.term().unwrap_or_default();
|
||||||
|
t.ws();
|
||||||
|
if t.b.get(t.i) == Some(&b'.') {
|
||||||
|
t.i += 1;
|
||||||
|
}
|
||||||
|
t.prefixes.insert(name, iri);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// sujeito
|
||||||
|
let Some(subj) =
|
||||||
|
(if t.b[t.i] == b':' && t.b.get(t.i + 1).map_or(true, |c| c.is_ascii_whitespace()) {
|
||||||
|
t.i += 1;
|
||||||
|
Some(t.prefixes.get("").cloned().unwrap_or_default())
|
||||||
|
} else {
|
||||||
|
t.term()
|
||||||
|
})
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let res = out.entry(subj).or_default();
|
||||||
|
loop {
|
||||||
|
let Some(pred) = t.term() else { return out };
|
||||||
|
let mut objs = vec![];
|
||||||
|
loop {
|
||||||
|
let Some(o) = t.term() else { return out };
|
||||||
|
objs.push(o);
|
||||||
|
t.ws();
|
||||||
|
if t.b.get(t.i) == Some(&b',') {
|
||||||
|
t.i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if pred == "a" {
|
||||||
|
res.types.extend(objs);
|
||||||
|
} else {
|
||||||
|
res.props.entry(pred).or_default().extend(objs);
|
||||||
|
}
|
||||||
|
t.ws();
|
||||||
|
match t.b.get(t.i) {
|
||||||
|
Some(b';') => {
|
||||||
|
t.i += 1;
|
||||||
|
t.ws();
|
||||||
|
// ";" final antes de "."
|
||||||
|
if t.b.get(t.i) == Some(&b'.') {
|
||||||
|
t.i += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(b'.') => {
|
||||||
|
t.i += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ leitor
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum Method {
|
||||||
|
Stored,
|
||||||
|
Deflate,
|
||||||
|
Zlib,
|
||||||
|
Snappy,
|
||||||
|
Lz4,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ImageStream {
|
||||||
|
urn: String,
|
||||||
|
chunk: u64,
|
||||||
|
per_bevy: u64,
|
||||||
|
method: Method,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct MapEntry {
|
||||||
|
start: u64,
|
||||||
|
len: u64,
|
||||||
|
target_off: u64,
|
||||||
|
target: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum Target {
|
||||||
|
Stream(usize),
|
||||||
|
Zero,
|
||||||
|
Fill(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Loc {
|
||||||
|
data_start: u64,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Aff4Source {
|
||||||
|
file: File,
|
||||||
|
entries: HashMap<String, Loc>,
|
||||||
|
streams: Vec<ImageStream>,
|
||||||
|
targets: Vec<Target>,
|
||||||
|
map: Vec<MapEntry>,
|
||||||
|
size: u64,
|
||||||
|
/// Metadados legíveis: caso, examinador, notas, hashes.
|
||||||
|
pub info: HashMap<String, String>,
|
||||||
|
cache: Mutex<Option<(usize, u64, Vec<u8>)>>,
|
||||||
|
index_cache: Mutex<HashMap<(usize, u64), std::sync::Arc<Vec<u8>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc(urn: &str) -> String {
|
||||||
|
urn.replace(':', "%3A").replace('/', "%2F")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn method_of(uri: &str) -> Method {
|
||||||
|
let u = uri.to_ascii_lowercase();
|
||||||
|
if u.contains("snappy") {
|
||||||
|
Method::Snappy
|
||||||
|
} else if u.contains("lz4") {
|
||||||
|
Method::Lz4
|
||||||
|
} else if u.contains("rfc1951") {
|
||||||
|
Method::Deflate
|
||||||
|
} else if u.contains("rfc1950") {
|
||||||
|
Method::Zlib
|
||||||
|
} else {
|
||||||
|
Method::Stored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Aff4Source {
|
||||||
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
|
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 entries = HashMap::new();
|
||||||
|
for i in 0..zip.len() {
|
||||||
|
let f = zip.by_index_raw(i).map_err(|e| Error::msg(e.to_string()))?;
|
||||||
|
entries.insert(
|
||||||
|
f.name().to_string(),
|
||||||
|
Loc {
|
||||||
|
data_start: f.data_start(),
|
||||||
|
size: f.compressed_size(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let turtle = {
|
||||||
|
let mut f = zip
|
||||||
|
.by_name("information.turtle")
|
||||||
|
.map_err(|_| Error::msg("AFF4 sem information.turtle"))?;
|
||||||
|
let mut s = String::new();
|
||||||
|
f.read_to_string(&mut s)?;
|
||||||
|
s
|
||||||
|
};
|
||||||
|
let res = parse_turtle(&turtle);
|
||||||
|
let img_t = format!("{NS}Image");
|
||||||
|
let disk_t = format!("{NS}DiskImage");
|
||||||
|
let ds = format!("{NS}dataStream");
|
||||||
|
let image = res
|
||||||
|
.values()
|
||||||
|
.find(|r| (r.is(&img_t) || r.is(&disk_t)) && r.get(&ds).is_some())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::msg("AFF4 sem objeto de imagem (aff4:Image com aff4:dataStream)")
|
||||||
|
})?;
|
||||||
|
let mut info = HashMap::new();
|
||||||
|
if let Some(v) = image.get(&format!("{NS}diskModel")) {
|
||||||
|
info.insert("Modelo do disco".to_string(), v.to_string());
|
||||||
|
}
|
||||||
|
for (k, label) in [
|
||||||
|
("caseNumber", "Número do caso"),
|
||||||
|
("examiner", "Examinador"),
|
||||||
|
("notes", "Notas"),
|
||||||
|
("evidenceNumber", "Número da evidência"),
|
||||||
|
("caseName", "Nome do caso"),
|
||||||
|
] {
|
||||||
|
if let Some(v) = res.values().find_map(|r| r.get(&format!("{NS}{k}"))) {
|
||||||
|
info.insert(label.to_string(), v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let data_urn = image.get(&ds).unwrap().to_string();
|
||||||
|
// Hashes gravados no fluxo de dados (ou no mapa).
|
||||||
|
for r in res
|
||||||
|
.values()
|
||||||
|
.filter(|r| r.get(&format!("{NS}hash")).is_some())
|
||||||
|
{
|
||||||
|
for h in r.props.get(&format!("{NS}hash")).into_iter().flatten() {
|
||||||
|
match h.len() {
|
||||||
|
32 if h.chars().all(|c| c.is_ascii_hexdigit()) => {
|
||||||
|
info.entry("md5".to_string()).or_insert_with(|| h.clone());
|
||||||
|
}
|
||||||
|
40 if h.chars().all(|c| c.is_ascii_hexdigit()) => {
|
||||||
|
info.entry("sha1".to_string()).or_insert_with(|| h.clone());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut me = Self {
|
||||||
|
file,
|
||||||
|
entries,
|
||||||
|
streams: vec![],
|
||||||
|
targets: vec![],
|
||||||
|
map: vec![],
|
||||||
|
size: 0,
|
||||||
|
info,
|
||||||
|
cache: Mutex::new(None),
|
||||||
|
index_cache: Mutex::new(HashMap::new()),
|
||||||
|
};
|
||||||
|
let top = res
|
||||||
|
.get(&data_urn)
|
||||||
|
.ok_or_else(|| Error::msg("AFF4: fluxo de dados da imagem não encontrado"))?;
|
||||||
|
let map_t = format!("{NS}Map");
|
||||||
|
if top.is(&map_t) {
|
||||||
|
me.size = top.num(&format!("{NS}size")).unwrap_or(0);
|
||||||
|
let mp = enc(&data_urn);
|
||||||
|
let idx_txt = me.read_entry(&format!("{mp}/idx"))?;
|
||||||
|
for line in String::from_utf8_lossy(&idx_txt).lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(sym) = line.strip_prefix(&format!("{NS}SymbolicStream")) {
|
||||||
|
let b = u8::from_str_radix(sym, 16)
|
||||||
|
.map_err(|_| Error::msg("AFF4: fluxo simbólico inválido"))?;
|
||||||
|
me.targets.push(Target::Fill(b));
|
||||||
|
} else if line == format!("{NS}Zero") {
|
||||||
|
me.targets.push(Target::Zero);
|
||||||
|
} else if line.starts_with(NS) {
|
||||||
|
me.targets.push(Target::Zero);
|
||||||
|
} else {
|
||||||
|
let s = me.load_stream(&res, line)?;
|
||||||
|
me.streams.push(s);
|
||||||
|
me.targets.push(Target::Stream(me.streams.len() - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let m = me.read_entry(&format!("{mp}/map"))?;
|
||||||
|
for c in m.chunks_exact(28) {
|
||||||
|
me.map.push(MapEntry {
|
||||||
|
start: u64::from_le_bytes(c[0..8].try_into().unwrap()),
|
||||||
|
len: u64::from_le_bytes(c[8..16].try_into().unwrap()),
|
||||||
|
target_off: u64::from_le_bytes(c[16..24].try_into().unwrap()),
|
||||||
|
target: u32::from_le_bytes(c[24..28].try_into().unwrap()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
me.map.sort_by_key(|e| e.start);
|
||||||
|
} else {
|
||||||
|
let s = me.load_stream(&res, &data_urn)?;
|
||||||
|
me.size = s.size;
|
||||||
|
me.streams.push(s);
|
||||||
|
me.targets.push(Target::Stream(0));
|
||||||
|
me.map = vec![MapEntry {
|
||||||
|
start: 0,
|
||||||
|
len: me.size,
|
||||||
|
target_off: 0,
|
||||||
|
target: 0,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
if me.size == 0 {
|
||||||
|
me.size = image.num(&format!("{NS}size")).unwrap_or(0);
|
||||||
|
}
|
||||||
|
Ok(me)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")))?;
|
||||||
|
Ok(ImageStream {
|
||||||
|
urn: urn.to_string(),
|
||||||
|
chunk: r.num(&format!("{NS}chunkSize")).unwrap_or(32768),
|
||||||
|
per_bevy: r.num(&format!("{NS}chunksInSegment")).unwrap_or(2048),
|
||||||
|
method: r
|
||||||
|
.get(&format!("{NS}compressionMethod"))
|
||||||
|
.map(method_of)
|
||||||
|
.unwrap_or(Method::Stored),
|
||||||
|
size: r.num(&format!("{NS}size")).unwrap_or(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MD5 do primeiro fluxo de blocos (o que o AFF4 grava em `aff4:hash`), para conferir a integridade.
|
||||||
|
pub fn verify_stream_md5(&self) -> Result<Option<bool>> {
|
||||||
|
let Some(want) = self.info.get("md5").cloned() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(st) = self.streams.first() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let mut h = MultiHasher::new();
|
||||||
|
let mut buf = vec![0u8; st.chunk as usize];
|
||||||
|
let mut pos = 0u64;
|
||||||
|
while pos < st.size {
|
||||||
|
let n = (st.size - pos).min(st.chunk) as usize;
|
||||||
|
self.read_stream(0, pos, &mut buf[..n])?;
|
||||||
|
h.update(&buf[..n]);
|
||||||
|
pos += n as u64;
|
||||||
|
}
|
||||||
|
Ok(Some(h.finalize().md5 == want))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 mut v = vec![0u8; loc.size as usize];
|
||||||
|
read_exact_at_file(&self.file, loc.data_start, &mut v)?;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chunk(&self, si: usize, chunk_no: u64) -> io::Result<Vec<u8>> {
|
||||||
|
{
|
||||||
|
let c = self.cache.lock().unwrap();
|
||||||
|
if let Some((s, n, d)) = c.as_ref() {
|
||||||
|
if *s == si && *n == chunk_no {
|
||||||
|
return Ok(d.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let st = &self.streams[si];
|
||||||
|
let (bevy, within) = (chunk_no / st.per_bevy, chunk_no % st.per_bevy);
|
||||||
|
let base = format!("{}/{:08}", enc(&st.urn), bevy);
|
||||||
|
let index = {
|
||||||
|
let mut ic = self.index_cache.lock().unwrap();
|
||||||
|
if let Some(i) = ic.get(&(si, bevy)) {
|
||||||
|
i.clone()
|
||||||
|
} else {
|
||||||
|
let loc = self.entries.get(&format!("{base}.index")).ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::NotFound, "índice do bevy ausente")
|
||||||
|
})?;
|
||||||
|
let mut v = vec![0u8; loc.size as usize];
|
||||||
|
read_exact_at_file(&self.file, loc.data_start, &mut v)?;
|
||||||
|
let a = std::sync::Arc::new(v);
|
||||||
|
ic.insert((si, bevy), a.clone());
|
||||||
|
a
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let e = index
|
||||||
|
.get(within as usize * 12..within as usize * 12 + 12)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "bloco fora do índice"))?;
|
||||||
|
let (off, len) = (
|
||||||
|
u64::from_le_bytes(e[0..8].try_into().unwrap()),
|
||||||
|
u32::from_le_bytes(e[8..12].try_into().unwrap()) as usize,
|
||||||
|
);
|
||||||
|
let loc = self
|
||||||
|
.entries
|
||||||
|
.get(&base)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "bevy ausente"))?;
|
||||||
|
let mut raw = vec![0u8; len];
|
||||||
|
read_exact_at_file(&self.file, loc.data_start + off, &mut raw)?;
|
||||||
|
let want = st.chunk.min(st.size.saturating_sub(chunk_no * st.chunk)) as usize;
|
||||||
|
let data = if len == st.chunk as usize || st.method == Method::Stored {
|
||||||
|
raw
|
||||||
|
} else {
|
||||||
|
let bad = |m: String| io::Error::new(io::ErrorKind::InvalidData, m);
|
||||||
|
match st.method {
|
||||||
|
Method::Snappy => snap::raw::Decoder::new()
|
||||||
|
.decompress_vec(&raw)
|
||||||
|
.map_err(|e| bad(format!("snappy: {e}")))?,
|
||||||
|
Method::Lz4 => lz4_flex::block::decompress(&raw, st.chunk as usize)
|
||||||
|
.map_err(|e| bad(format!("lz4: {e}")))?,
|
||||||
|
Method::Deflate => {
|
||||||
|
let mut v = Vec::with_capacity(st.chunk as usize);
|
||||||
|
flate2::read::DeflateDecoder::new(&raw[..])
|
||||||
|
.read_to_end(&mut v)
|
||||||
|
.map_err(|e| bad(format!("deflate: {e}")))?;
|
||||||
|
v
|
||||||
|
}
|
||||||
|
Method::Zlib => {
|
||||||
|
let mut v = Vec::with_capacity(st.chunk as usize);
|
||||||
|
flate2::read::ZlibDecoder::new(&raw[..])
|
||||||
|
.read_to_end(&mut v)
|
||||||
|
.map_err(|e| bad(format!("zlib: {e}")))?;
|
||||||
|
v
|
||||||
|
}
|
||||||
|
Method::Stored => unreachable!(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut data = data;
|
||||||
|
data.resize(want.max(data.len().min(st.chunk as usize)), 0);
|
||||||
|
*self.cache.lock().unwrap() = Some((si, chunk_no, data.clone()));
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_stream(&self, si: usize, off: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||||
|
let cs = self.streams[si].chunk;
|
||||||
|
let mut done = 0;
|
||||||
|
while done < buf.len() {
|
||||||
|
let pos = off + done as u64;
|
||||||
|
let (cn, within) = (pos / cs, (pos % cs) as usize);
|
||||||
|
let d = self.chunk(si, cn)?;
|
||||||
|
if within >= d.len() {
|
||||||
|
buf[done..].fill(0);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let n = (d.len() - within).min(buf.len() - done);
|
||||||
|
buf[done..done + n].copy_from_slice(&d[within..within + n]);
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_exact_at_file(f: &File, off: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
std::os::unix::fs::FileExt::read_exact_at(f, buf, off)
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let mut done = 0;
|
||||||
|
while done < buf.len() {
|
||||||
|
let n =
|
||||||
|
std::os::windows::fs::FileExt::seek_read(f, &mut buf[done..], off + done as u64)?;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::UnexpectedEof,
|
||||||
|
"AFF4 truncado",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for Aff4Source {
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.size || buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let want = buf.len().min((self.size - offset) as usize);
|
||||||
|
let mut done = 0usize;
|
||||||
|
while done < want {
|
||||||
|
let pos = offset + done as u64;
|
||||||
|
let i = self.map.partition_point(|e| e.start + e.len <= pos);
|
||||||
|
let Some(e) = self.map.get(i).filter(|e| e.start <= pos) else {
|
||||||
|
// vão do mapa: zeros até a próxima entrada
|
||||||
|
let next = self.map.get(i).map(|e| e.start).unwrap_or(self.size);
|
||||||
|
let n = ((next - pos) as usize).min(want - done);
|
||||||
|
buf[done..done + n].fill(0);
|
||||||
|
done += n;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let n = ((e.start + e.len - pos) as usize).min(want - done);
|
||||||
|
let toff = e.target_off + (pos - e.start);
|
||||||
|
match self.targets.get(e.target as usize) {
|
||||||
|
Some(Target::Stream(si)) => {
|
||||||
|
self.read_stream(*si, toff, &mut buf[done..done + n])?
|
||||||
|
}
|
||||||
|
Some(Target::Fill(b)) => buf[done..done + n].fill(*b),
|
||||||
|
_ => buf[done..done + n].fill(0),
|
||||||
|
}
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ gravação
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Aff4Options {
|
||||||
|
pub case_name: String,
|
||||||
|
pub examiner: String,
|
||||||
|
pub notes: String,
|
||||||
|
pub evidence_number: String,
|
||||||
|
pub chunk_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Aff4Options {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
case_name: String::new(),
|
||||||
|
examiner: String::new(),
|
||||||
|
notes: String::new(),
|
||||||
|
evidence_number: String::new(),
|
||||||
|
chunk_size: 32768,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_uuid() -> String {
|
||||||
|
let mut b = [0u8; 16];
|
||||||
|
let _ = std::fs::File::open("/dev/urandom").and_then(|mut f| f.read_exact(&mut b));
|
||||||
|
if b == [0u8; 16] {
|
||||||
|
let t = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(1);
|
||||||
|
b = (t ^ (std::process::id() as u128) << 64).to_le_bytes();
|
||||||
|
}
|
||||||
|
b[6] = (b[6] & 0x0F) | 0x40;
|
||||||
|
b[8] = (b[8] & 0x3F) | 0x80;
|
||||||
|
let h: Vec<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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 days = secs.div_euclid(86400);
|
||||||
|
let rem = secs.rem_euclid(86400);
|
||||||
|
let z = days + 719468;
|
||||||
|
let era = z.div_euclid(146097);
|
||||||
|
let doe = z.rem_euclid(146097);
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||||
|
let y = yoe + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
|
let y = if m <= 2 { y + 1 } else { y };
|
||||||
|
format!(
|
||||||
|
"{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}.000Z",
|
||||||
|
rem / 3600,
|
||||||
|
rem % 3600 / 60,
|
||||||
|
rem % 60
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn esc(s: &str) -> String {
|
||||||
|
s.replace('\\', "\\\\")
|
||||||
|
.replace('"', "\\\"")
|
||||||
|
.replace('\n', "\\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grava uma imagem AFF4 linear (um fluxo de blocos snappy) de `src`.
|
||||||
|
pub fn write_aff4(
|
||||||
|
src: &dyn Source,
|
||||||
|
dest: &Path,
|
||||||
|
opts: &Aff4Options,
|
||||||
|
cancelled: &(dyn Fn() -> bool + Sync),
|
||||||
|
progress: &mut dyn FnMut(u64, u64),
|
||||||
|
) -> Result<ImageReport> {
|
||||||
|
let out = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(dest)
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::msg(format!(
|
||||||
|
"não foi possível criar {}: {e} (o arquivo não pode existir)",
|
||||||
|
dest.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
crate::owner::give_to_user(dest);
|
||||||
|
let total = src.size();
|
||||||
|
let chunk = opts.chunk_size as u64;
|
||||||
|
let per_bevy = 2048u64;
|
||||||
|
let volume = format!("aff4://{}", new_uuid());
|
||||||
|
let stream = format!("aff4://{}", new_uuid());
|
||||||
|
let image = format!("aff4://{}", new_uuid());
|
||||||
|
let mut zip = zip::ZipWriter::new(out);
|
||||||
|
let stored = zip::write::SimpleFileOptions::default()
|
||||||
|
.compression_method(zip::CompressionMethod::Stored)
|
||||||
|
.large_file(true);
|
||||||
|
let result = (|| -> Result<ImageReport> {
|
||||||
|
zip.start_file("container.description", stored)
|
||||||
|
.map_err(zerr)?;
|
||||||
|
zip.write_all(volume.as_bytes())?;
|
||||||
|
zip.start_file("version.txt", stored).map_err(zerr)?;
|
||||||
|
zip.write_all(format!("major=1\nminor=2\ntool=Guara {}\n", crate::VERSION).as_bytes())?;
|
||||||
|
|
||||||
|
let mut rep = ImageReport::default();
|
||||||
|
let mut hasher = MultiHasher::new();
|
||||||
|
let mut buf = vec![0u8; chunk as usize];
|
||||||
|
let mut pos = 0u64;
|
||||||
|
let mut chunk_no = 0u64;
|
||||||
|
let mut enc_snap = snap::raw::Encoder::new();
|
||||||
|
while pos < total {
|
||||||
|
let bevy = chunk_no / per_bevy;
|
||||||
|
let name = format!("{}/{:08}", enc(&stream), bevy);
|
||||||
|
zip.start_file(name.as_str(), stored).map_err(zerr)?;
|
||||||
|
let mut index: Vec<u8> = vec![];
|
||||||
|
let mut boff = 0u64;
|
||||||
|
for _ in 0..per_bevy {
|
||||||
|
if pos >= total {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if cancelled() {
|
||||||
|
rep.cancelled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let want = (total - pos).min(chunk) as usize;
|
||||||
|
let b = &mut buf[..want];
|
||||||
|
if read_exact_at(src, pos, b).is_err() {
|
||||||
|
let mut o = 0;
|
||||||
|
while o < want {
|
||||||
|
let n = 512.min(want - o);
|
||||||
|
if read_exact_at(src, pos + o as u64, &mut b[o..o + n]).is_err() {
|
||||||
|
b[o..o + n].fill(0);
|
||||||
|
match rep.bad_ranges.last_mut() {
|
||||||
|
Some(l) if l.0 + l.1 == pos + o as u64 => l.1 += n as u64,
|
||||||
|
_ => rep.bad_ranges.push((pos + o as u64, n as u64)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasher.update(b);
|
||||||
|
// bloco curto (último) é guardado como está; comprimido só se ficar menor
|
||||||
|
let comp = enc_snap
|
||||||
|
.compress_vec(b)
|
||||||
|
.map_err(|e| Error::msg(e.to_string()))?;
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
zip.write_all(data)?;
|
||||||
|
index.extend_from_slice(&boff.to_le_bytes());
|
||||||
|
index.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||||
|
boff += data.len() as u64;
|
||||||
|
pos += want as u64;
|
||||||
|
chunk_no += 1;
|
||||||
|
progress(pos, total);
|
||||||
|
}
|
||||||
|
zip.start_file(format!("{name}.index").as_str(), stored)
|
||||||
|
.map_err(zerr)?;
|
||||||
|
zip.write_all(&index)?;
|
||||||
|
if rep.cancelled {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let h = hasher.finalize();
|
||||||
|
rep.bytes = pos;
|
||||||
|
rep.md5 = h.md5.clone();
|
||||||
|
rep.sha1 = h.sha1.clone();
|
||||||
|
rep.sha256 = h.sha256.clone();
|
||||||
|
|
||||||
|
let now = iso_now();
|
||||||
|
let sectors = total.div_ceil(512);
|
||||||
|
let ttl = format!(
|
||||||
|
"@prefix aff4: <http://aff4.org/Schema#> .\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n\n\
|
||||||
|
<{stream}>\n a aff4:ImageStream ;\n aff4:chunkSize \"{chunk}\"^^xsd:int ;\n aff4:chunksInSegment \"{per_bevy}\"^^xsd:int ;\n aff4:compressionMethod <http://code.google.com/p/snappy/> ;\n aff4:hash \"{sha1}\"^^aff4:SHA1 , \"{md5}\"^^aff4:MD5 ;\n aff4:size \"{total}\"^^xsd:long ;\n aff4:stored <{volume}> ;\n aff4:target <{image}> ;\n aff4:version \"1\"^^xsd:int .\n\n\
|
||||||
|
<{image}>\n a aff4:ContiguousImage , aff4:DiskImage , aff4:Image ;\n aff4:blockSize \"512\"^^xsd:int ;\n aff4:dataStream <{stream}> ;\n aff4:sectorCount \"{sectors}\"^^xsd:long ;\n aff4:size \"{total}\"^^xsd:long ;\n aff4:stored <{volume}> .\n\n\
|
||||||
|
<aff4://{notes_id}>\n a aff4:CaseNotes ;\n aff4:caseNumber \"{case}\" ;\n aff4:evidenceNumber \"{ev}\" ;\n aff4:examiner \"{ex}\" ;\n aff4:notes \"{notes}\" ;\n aff4:stored <{volume}> ;\n aff4:target <{image}> ;\n aff4:timestamp \"{now}\"^^xsd:dateTime .\n\n\
|
||||||
|
<{volume}>\n a aff4:ZipVolume ;\n aff4:contains <{stream}> , <{image}> ;\n aff4:creationTime \"{now}\"^^xsd:dateTime ;\n aff4:interface aff4:Volume ;\n aff4:stored \"{fname}\" .\n",
|
||||||
|
sha1 = h.sha1,
|
||||||
|
md5 = h.md5,
|
||||||
|
notes_id = new_uuid(),
|
||||||
|
case = esc(&opts.case_name),
|
||||||
|
ev = esc(&opts.evidence_number),
|
||||||
|
ex = esc(&opts.examiner),
|
||||||
|
notes = esc(&opts.notes),
|
||||||
|
fname = esc(&dest.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default()),
|
||||||
|
);
|
||||||
|
zip.start_file("information.turtle", stored).map_err(zerr)?;
|
||||||
|
zip.write_all(ttl.as_bytes())?;
|
||||||
|
Ok(rep)
|
||||||
|
})();
|
||||||
|
match result {
|
||||||
|
Ok(rep) => {
|
||||||
|
zip.finish().map_err(zerr)?;
|
||||||
|
crate::owner::write_owned(
|
||||||
|
&dest.with_extension("map.json"),
|
||||||
|
serde_json::to_vec_pretty(&rep)?,
|
||||||
|
)?;
|
||||||
|
Ok(rep)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
drop(zip);
|
||||||
|
let _ = std::fs::remove_file(dest);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zerr(e: zip::result::ZipError) -> Error {
|
||||||
|
Error::msg(format!("AFF4/ZIP: {e}"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,541 @@
|
|||||||
|
//! Monta o disco virtual a partir do que o usuário informou: membros extras (RAID/LVM) e
|
||||||
|
//! senhas ou chaves para volumes cifrados. É a única porta de entrada para varrer e recuperar
|
||||||
|
//! discos que não são "simples".
|
||||||
|
|
||||||
|
use crate::crypt::{self, Secret, UnlockError};
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::partition;
|
||||||
|
use crate::source::{open_path, Source, SubSource};
|
||||||
|
use crate::types::{FsKind, PartInfo};
|
||||||
|
use crate::volume::{Overlay, Region};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||||
|
pub enum SecretInput {
|
||||||
|
Password {
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
Recovery {
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// Caminho de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS).
|
||||||
|
Keyfile {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
/// Chave do volume em hexadecimal.
|
||||||
|
Key {
|
||||||
|
hex: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chave de volume guardada na sessão (só quando o usuário pede para lembrar).
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SavedKey {
|
||||||
|
pub start: u64,
|
||||||
|
pub kind: String,
|
||||||
|
pub key_hex: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RAID sem metadados: o que o usuário sabe; o resto é descoberto.
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct RaidSpec {
|
||||||
|
pub level: Option<crate::raid::Level>,
|
||||||
|
/// Tamanho da faixa em KiB.
|
||||||
|
pub chunk_kb: Option<u64>,
|
||||||
|
pub layout: Option<crate::raid::Layout>,
|
||||||
|
/// Ordem dos discos: índice 0 é o alvo principal, depois `members` na ordem dada.
|
||||||
|
pub order: Option<Vec<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Assembly {
|
||||||
|
/// Trata o alvo e `members` como discos de um RAID sem metadados (parâmetros descobertos).
|
||||||
|
#[serde(default)]
|
||||||
|
pub raid: Option<RaidSpec>,
|
||||||
|
/// Outros discos ou imagens do mesmo conjunto (RAID/LVM), além do alvo principal.
|
||||||
|
#[serde(default)]
|
||||||
|
pub members: Vec<String>,
|
||||||
|
/// Senhas e chaves: o `snapshot` da sessão as descarta antes de gravar.
|
||||||
|
#[serde(default)]
|
||||||
|
pub secrets: Vec<SecretInput>,
|
||||||
|
/// Guarda as chaves de volume derivadas na sessão, para reabrir sem pedir a senha de novo.
|
||||||
|
#[serde(default)]
|
||||||
|
pub remember_keys: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub keys: Vec<SavedKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Assembly {
|
||||||
|
pub fn is_plain(&self) -> bool {
|
||||||
|
self.members.is_empty()
|
||||||
|
&& self.secrets.is_empty()
|
||||||
|
&& self.keys.is_empty()
|
||||||
|
&& self.raid.is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct UnlockInfo {
|
||||||
|
pub start: u64,
|
||||||
|
pub len: u64,
|
||||||
|
pub kind: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct LockedInfo {
|
||||||
|
pub start: u64,
|
||||||
|
pub kind: String,
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Assembled {
|
||||||
|
pub source: Arc<dyn Source>,
|
||||||
|
pub unlocked: Vec<UnlockInfo>,
|
||||||
|
pub locked: Vec<LockedInfo>,
|
||||||
|
/// Chaves derivadas, para guardar na sessão se o usuário permitir.
|
||||||
|
pub keys: Vec<SavedKey>,
|
||||||
|
/// Volumes lógicos / partições extras que não estão na tabela do disco.
|
||||||
|
pub extra_parts: Vec<PartInfo>,
|
||||||
|
/// Partições reais que são só contêineres (membros de RAID/LVM já remontados).
|
||||||
|
pub covered: Vec<u64>,
|
||||||
|
/// O disco principal é só um membro de RAID: não varrer o disco cru.
|
||||||
|
pub skip_primary: bool,
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_secrets(a: &Assembly) -> Result<Vec<Secret>> {
|
||||||
|
let mut v = vec![];
|
||||||
|
for s in &a.secrets {
|
||||||
|
v.push(match s {
|
||||||
|
SecretInput::Password { value } => Secret::Password(value.clone()),
|
||||||
|
SecretInput::Recovery { value } => Secret::RecoveryKey(value.clone()),
|
||||||
|
SecretInput::Keyfile { path } => Secret::KeyFile(std::fs::read(path).map_err(|e| {
|
||||||
|
Error::msg(format!(
|
||||||
|
"não foi possível ler o arquivo de chave {path}: {e}"
|
||||||
|
))
|
||||||
|
})?),
|
||||||
|
SecretInput::Key { hex } => Secret::VolumeKey(
|
||||||
|
crypt::from_hex(hex)
|
||||||
|
.ok_or_else(|| Error::msg("chave do volume em hexadecimal inválida"))?,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind_name(fs: FsKind) -> &'static str {
|
||||||
|
match fs {
|
||||||
|
FsKind::Bitlocker => "bitlocker",
|
||||||
|
FsKind::Luks => "luks",
|
||||||
|
FsKind::Corestorage => "filevault",
|
||||||
|
_ => "?",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_unlock(window: Arc<dyn Source>, fs: FsKind, secrets: &[Secret]) -> crypt::UnlockResult {
|
||||||
|
match fs {
|
||||||
|
FsKind::Bitlocker => crypt::bitlocker::unlock(window, secrets),
|
||||||
|
FsKind::Luks => crypt::luks::unlock(window, secrets),
|
||||||
|
FsKind::Corestorage => crypt::filevault::unlock(window, secrets),
|
||||||
|
_ => Err(UnlockError::Unsupported("tipo".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Abre `path` e aplica a montagem pedida.
|
||||||
|
pub fn assemble(path: &str, asm: &Assembly) -> Result<Assembled> {
|
||||||
|
let base = open_path(Path::new(path)).map_err(|e| Error::msg(format!("{path}: {e}")))?;
|
||||||
|
assemble_source(base, asm)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIB: u64 = 1 << 20;
|
||||||
|
|
||||||
|
/// Coloca `src` em `start`; se cair dentro de uma região virtual, a substitui por dentro.
|
||||||
|
fn patch(regions: &mut Vec<Region>, start: u64, src: Arc<dyn Source>, label: String) {
|
||||||
|
let len = src.size();
|
||||||
|
if let Some(r) = regions
|
||||||
|
.iter_mut()
|
||||||
|
.find(|r| r.start <= start && start < r.start + r.len)
|
||||||
|
{
|
||||||
|
let inner = Region {
|
||||||
|
start: start - r.start,
|
||||||
|
len,
|
||||||
|
src,
|
||||||
|
label: label.clone(),
|
||||||
|
};
|
||||||
|
let wrapped = Overlay::new(r.src.clone(), vec![inner]);
|
||||||
|
if start == r.start {
|
||||||
|
r.len = r.len.max(len);
|
||||||
|
}
|
||||||
|
r.src = Arc::new(wrapped);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
regions.push(Region {
|
||||||
|
start,
|
||||||
|
len,
|
||||||
|
src,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Candidato a membro de RAID/LVM: uma janela de disco (inteiro ou partição).
|
||||||
|
struct Window {
|
||||||
|
src: Arc<dyn Source>,
|
||||||
|
/// Início no espaço do disco virtual, se a janela faz parte dele.
|
||||||
|
primary_start: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 layout = partition::analyze(src.as_ref(), false);
|
||||||
|
for p in &layout.partitions {
|
||||||
|
let len = if p.len == 0 {
|
||||||
|
src.size().saturating_sub(p.start)
|
||||||
|
} else {
|
||||||
|
p.len
|
||||||
|
};
|
||||||
|
v.push(Window {
|
||||||
|
src: Arc::new(SubSource::new(src.clone(), p.start, len)),
|
||||||
|
primary_start: primary.then_some(p.start),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assembled> {
|
||||||
|
let secrets = to_secrets(asm)?;
|
||||||
|
let mut regions: Vec<Region> = vec![];
|
||||||
|
let mut out = Assembled {
|
||||||
|
source: base.clone(),
|
||||||
|
unlocked: vec![],
|
||||||
|
locked: vec![],
|
||||||
|
keys: vec![],
|
||||||
|
extra_parts: vec![],
|
||||||
|
covered: vec![],
|
||||||
|
skip_primary: false,
|
||||||
|
notes: vec![],
|
||||||
|
};
|
||||||
|
let mut members: Vec<Arc<dyn Source>> = vec![];
|
||||||
|
for m in &asm.members {
|
||||||
|
members.push(open_path(Path::new(m)).map_err(|e| Error::msg(format!("{m}: {e}")))?);
|
||||||
|
}
|
||||||
|
let mut next_virtual = base.size().div_ceil(MIB) * MIB + MIB;
|
||||||
|
if let Some(spec) = &asm.raid {
|
||||||
|
let mut disks: Vec<Arc<dyn Source>> = vec![base.clone()];
|
||||||
|
disks.extend(members.iter().cloned());
|
||||||
|
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)?);
|
||||||
|
out.notes.push(format!(
|
||||||
|
"RAID montado a partir de {} disco(s) sem metadados.",
|
||||||
|
disks.len()
|
||||||
|
));
|
||||||
|
out.skip_primary = true;
|
||||||
|
members.clear();
|
||||||
|
add_virtual(
|
||||||
|
&mut regions,
|
||||||
|
&mut next_virtual,
|
||||||
|
&mut out,
|
||||||
|
src,
|
||||||
|
"RAID".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut done_containers: std::collections::BTreeSet<String> = Default::default();
|
||||||
|
let mut tried_pool = false;
|
||||||
|
// Rodadas: o que foi aberto/montado pode conter outro volume cifrado, RAID ou LVM.
|
||||||
|
for round in 0..4 {
|
||||||
|
let current: Arc<dyn Source> = Arc::new(Overlay::new(base.clone(), regions.clone()));
|
||||||
|
let layout = partition::analyze(current.as_ref(), false);
|
||||||
|
let mut all_parts: Vec<PartInfo> = layout.partitions.clone();
|
||||||
|
for p in &out.extra_parts {
|
||||||
|
if !all_parts.iter().any(|q| q.start == p.start) {
|
||||||
|
all_parts.push(p.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut progress = false;
|
||||||
|
|
||||||
|
// 1) RAID e LVM: reúne janelas do disco principal e dos membros extras.
|
||||||
|
if round == 0 || !tried_pool || !out.extra_parts.is_empty() {
|
||||||
|
let mut pool: Vec<Window> = vec![];
|
||||||
|
pool.push(Window {
|
||||||
|
src: current.clone(),
|
||||||
|
primary_start: Some(0),
|
||||||
|
});
|
||||||
|
for p in &all_parts {
|
||||||
|
if matches!(p.fs, FsKind::Raid | FsKind::Lvm) {
|
||||||
|
let len = if p.len == 0 {
|
||||||
|
current.size().saturating_sub(p.start)
|
||||||
|
} else {
|
||||||
|
p.len
|
||||||
|
};
|
||||||
|
pool.push(Window {
|
||||||
|
src: Arc::new(SubSource::new(current.clone(), p.start, len)),
|
||||||
|
primary_start: Some(p.start),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for m in &members {
|
||||||
|
pool.extend(windows_of(m, false));
|
||||||
|
}
|
||||||
|
tried_pool = true;
|
||||||
|
// RAID
|
||||||
|
let mut md: Vec<(Arc<dyn Source>, crate::raid::MdSuper)> = vec![];
|
||||||
|
let mut md_primary: Vec<u64> = vec![];
|
||||||
|
for w in &pool {
|
||||||
|
if let Some(sup) = crate::raid::read_md_super(w.src.as_ref()) {
|
||||||
|
if let Some(st) = w.primary_start {
|
||||||
|
md_primary.push(st);
|
||||||
|
}
|
||||||
|
md.push((w.src.clone(), sup));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let new_md: Vec<_> = md
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, s)| {
|
||||||
|
!done_containers.contains(&format!("md{}", crypt::to_hex(&s.uuid)))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !new_md.is_empty() {
|
||||||
|
for (_, s) in &new_md {
|
||||||
|
done_containers.insert(format!("md{}", crypt::to_hex(&s.uuid)));
|
||||||
|
}
|
||||||
|
for res in crate::raid::assemble_md(new_md) {
|
||||||
|
match res {
|
||||||
|
Ok(a) => {
|
||||||
|
let label = format!(
|
||||||
|
"{} {}",
|
||||||
|
a.info.level.name(),
|
||||||
|
if a.name.is_empty() {
|
||||||
|
a.uuid.clone()
|
||||||
|
} else {
|
||||||
|
a.name.clone()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if a.missing > 0 {
|
||||||
|
out.notes.push(format!(
|
||||||
|
"{label}: montado sem {} membro(s) (modo degradado).",
|
||||||
|
a.missing
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
out.notes.push(format!("{label} montado."));
|
||||||
|
}
|
||||||
|
add_virtual(
|
||||||
|
&mut regions,
|
||||||
|
&mut next_virtual,
|
||||||
|
&mut out,
|
||||||
|
a.source.clone(),
|
||||||
|
label,
|
||||||
|
);
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
Err(e) => out.notes.push(format!("RAID não montado: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.covered.extend(md_primary);
|
||||||
|
}
|
||||||
|
// LVM
|
||||||
|
let mut pvs: Vec<(Arc<dyn Source>, crate::lvm::PvInfo)> = vec![];
|
||||||
|
let mut pv_primary: Vec<u64> = vec![];
|
||||||
|
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();
|
||||||
|
for (r, s) in regions.iter().zip(md_srcs) {
|
||||||
|
lvm_pool.push((s, Some(r.start)));
|
||||||
|
}
|
||||||
|
for (src, st) in lvm_pool {
|
||||||
|
if let Some(pv) = crate::lvm::read_pv(src.as_ref()) {
|
||||||
|
if !done_containers.insert(format!("pv{}", pv.uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(st) = st {
|
||||||
|
pv_primary.push(st);
|
||||||
|
}
|
||||||
|
pvs.push((src, pv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pvs.is_empty() {
|
||||||
|
for vg in crate::lvm::assemble_vgs(pvs) {
|
||||||
|
for w in &vg.warnings {
|
||||||
|
out.notes.push(format!("LVM {}: {w}", vg.name));
|
||||||
|
}
|
||||||
|
for (name, lv) in vg.lvs {
|
||||||
|
add_virtual(
|
||||||
|
&mut regions,
|
||||||
|
&mut next_virtual,
|
||||||
|
&mut out,
|
||||||
|
lv,
|
||||||
|
format!("LVM {}/{}", vg.name, name),
|
||||||
|
);
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.covered.extend(pv_primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Desbloqueio de volumes cifrados.
|
||||||
|
let current: Arc<dyn Source> = Arc::new(Overlay::new(base.clone(), regions.clone()));
|
||||||
|
let layout = partition::analyze(current.as_ref(), false);
|
||||||
|
let mut all_parts: Vec<PartInfo> = layout.partitions.clone();
|
||||||
|
for p in &out.extra_parts {
|
||||||
|
if !all_parts.iter().any(|q| q.start == p.start) {
|
||||||
|
all_parts.push(p.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.locked.clear();
|
||||||
|
for p in &all_parts {
|
||||||
|
if p.encrypted.is_none() || out.unlocked.iter().any(|u| u.start == p.start) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let len = if p.len == 0 {
|
||||||
|
current.size().saturating_sub(p.start)
|
||||||
|
} else {
|
||||||
|
p.len
|
||||||
|
};
|
||||||
|
let window: Arc<dyn Source> = Arc::new(SubSource::new(current.clone(), p.start, len));
|
||||||
|
let mut all = secrets.clone();
|
||||||
|
for k in asm.keys.iter().filter(|k| k.start == p.start) {
|
||||||
|
if let Some(b) = crypt::from_hex(&k.key_hex) {
|
||||||
|
all.push(Secret::VolumeKey(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match try_unlock(window, p.fs, &all) {
|
||||||
|
Ok(u) => {
|
||||||
|
let ulen = u.source.size();
|
||||||
|
out.unlocked.push(UnlockInfo {
|
||||||
|
start: p.start,
|
||||||
|
len: ulen,
|
||||||
|
kind: kind_name(p.fs).into(),
|
||||||
|
description: u.description.clone(),
|
||||||
|
});
|
||||||
|
if let Some(k) = &u.volume_key {
|
||||||
|
out.keys.push(SavedKey { start: p.start, kind: kind_name(p.fs).into(), key_hex: crypt::to_hex(k) });
|
||||||
|
}
|
||||||
|
patch(&mut regions, p.start, u.source, u.description);
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
Err(e) => out.locked.push(LockedInfo {
|
||||||
|
start: p.start,
|
||||||
|
kind: kind_name(p.fs).into(),
|
||||||
|
reason: match e {
|
||||||
|
UnlockError::WrongSecret if secrets.is_empty() && asm.keys.is_empty() => "volume cifrado: informe a senha, a chave de recuperação ou o arquivo de chave".into(),
|
||||||
|
other => other.to_string(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !progress {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !regions.is_empty() {
|
||||||
|
out.source = Arc::new(Overlay::new(base, regions));
|
||||||
|
// Atualiza o que se sabe sobre as partições virtuais (algumas foram desbloqueadas depois).
|
||||||
|
for p in &mut out.extra_parts {
|
||||||
|
if let Some((fs, len)) = partition::detect_fs(out.source.as_ref(), p.start) {
|
||||||
|
p.fs = fs;
|
||||||
|
p.encrypted = fs.encrypted().map(String::from);
|
||||||
|
if let Some(l) = len {
|
||||||
|
p.len = p.len.min(l).max(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.covered.sort_unstable();
|
||||||
|
out.covered.dedup();
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
) {
|
||||||
|
let start = *next;
|
||||||
|
let len = src.size();
|
||||||
|
if len == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*next = (start + len).div_ceil(MIB) * MIB + MIB;
|
||||||
|
let inner = partition::analyze(src.as_ref(), false);
|
||||||
|
if inner.partitions.is_empty() {
|
||||||
|
out.extra_parts.push(PartInfo {
|
||||||
|
index: 0,
|
||||||
|
start,
|
||||||
|
len,
|
||||||
|
fs: FsKind::Unknown,
|
||||||
|
label: Some(label.clone()),
|
||||||
|
lost: false,
|
||||||
|
encrypted: None,
|
||||||
|
table_type: Some(label.clone()),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for p in inner.partitions {
|
||||||
|
let mut q = p.clone();
|
||||||
|
q.start += start;
|
||||||
|
q.label = q.label.or_else(|| Some(label.clone()));
|
||||||
|
out.extra_parts.push(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
regions.push(Region {
|
||||||
|
start,
|
||||||
|
len,
|
||||||
|
src,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
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 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();
|
||||||
|
return Ok(RaidParams::simple(level, chunk * 1024, layout, members));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cands = crate::raid_detect::detect(disks, &|| false);
|
||||||
|
let c = cands
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| {
|
||||||
|
spec.level.map_or(true, |l| c.level == l)
|
||||||
|
&& spec.chunk_kb.map_or(true, |k| c.chunk == k * 1024)
|
||||||
|
&& spec.layout.map_or(true, |l| c.layout == l)
|
||||||
|
&& spec.order.as_ref().map_or(true, |o| *o == c.order)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| Error::msg("não foi possível descobrir os parâmetros do RAID (nenhuma combinação produziu um sistema de arquivos coerente)"))?;
|
||||||
|
notes.push(format!(
|
||||||
|
"Parâmetros do RAID descobertos: {}, faixa de {} KiB, layout {:?}, ordem dos discos {:?} (pontuação {:.1}).",
|
||||||
|
c.level.name(),
|
||||||
|
c.chunk / 1024,
|
||||||
|
c.layout,
|
||||||
|
c.order,
|
||||||
|
c.score
|
||||||
|
));
|
||||||
|
Ok(c.params(disks))
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ pub fn carve_range(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let scan_len = got.min(CHUNK).min((end - pos) as usize);
|
let scan_len = got.min(CHUNK).min((end - pos) as usize);
|
||||||
|
ev.chunk(pos, &buf[..scan_len]);
|
||||||
let mut i = 0usize;
|
let mut i = 0usize;
|
||||||
while i < scan_len {
|
while i < scan_len {
|
||||||
let abs = pos + i as u64;
|
let abs = pos + i as u64;
|
||||||
@@ -78,10 +79,12 @@ pub fn carve_range(
|
|||||||
|| cfg.categories.contains(&category_for_ext(hit.ext));
|
|| cfg.categories.contains(&category_for_ext(hit.ext));
|
||||||
if hit.len >= MIN_LEN && allowed {
|
if hit.len >= MIN_LEN && allowed {
|
||||||
let action = ev.hit(abs, &hit);
|
let action = ev.hit(abs, &hit);
|
||||||
if matches!(action, HitAction::Skip)
|
match action {
|
||||||
&& hit.confidence != crate::types::Confidence::Low
|
HitAction::Skip if hit.confidence != crate::types::Confidence::Low => {
|
||||||
{
|
skip_until = abs + hit.len;
|
||||||
skip_until = abs + hit.len;
|
}
|
||||||
|
HitAction::SkipLen(n) => skip_until = abs + n,
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +112,8 @@ pub enum CarveEnd {
|
|||||||
/// Receptor de eventos do carving.
|
/// Receptor de eventos do carving.
|
||||||
pub trait CarveEvents {
|
pub trait CarveEvents {
|
||||||
fn advance(&mut self, bytes: u64);
|
fn advance(&mut self, bytes: u64);
|
||||||
|
/// Cada trecho lido, antes de procurar assinaturas (para indexar o que serve a recomposição).
|
||||||
|
fn chunk(&mut self, _abs: u64, _data: &[u8]) {}
|
||||||
fn hit(&mut self, offset: u64, hit: &Hit) -> HitAction;
|
fn hit(&mut self, offset: u64, hit: &Hit) -> HitAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +121,8 @@ pub trait CarveEvents {
|
|||||||
pub enum HitAction {
|
pub enum HitAction {
|
||||||
/// Pula o conteúdo do arquivo reconhecido (comportamento padrão).
|
/// Pula o conteúdo do arquivo reconhecido (comportamento padrão).
|
||||||
Skip,
|
Skip,
|
||||||
|
/// Pula só estes bytes (arquivo cujo resto não é confiável).
|
||||||
|
SkipLen(u64),
|
||||||
/// Segue setor a setor (acerto descartado).
|
/// Segue setor a setor (acerto descartado).
|
||||||
Continue,
|
Continue,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,529 @@
|
|||||||
|
//! BitLocker (Windows 7 em diante, inclusive BitLocker To Go): AES-CBC com ou sem difusor
|
||||||
|
//! Elephant, e AES-XTS. Protetores: senha, chave de recuperação, chave de inicialização (.BEK),
|
||||||
|
//! chave limpa (proteção suspensa) e chave do volume já conhecida. TPM não é suportado.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::source::{read_vec, SubSource};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
fn le16(b: &[u8], o: usize) -> usize {
|
||||||
|
u16::from_le_bytes([b[o], b[o + 1]]) as usize
|
||||||
|
}
|
||||||
|
fn le32(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn le64(b: &[u8], o: usize) -> u64 {
|
||||||
|
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
const GUID_NORMAL: [u8; 16] = [
|
||||||
|
0x3b, 0xd6, 0x67, 0x49, 0x29, 0x2e, 0xd8, 0x4a, 0x83, 0x99, 0xf6, 0xa3, 0x39, 0xe3, 0xd0, 0x01,
|
||||||
|
];
|
||||||
|
const GUID_EOW: [u8; 16] = [
|
||||||
|
0x3b, 0x4d, 0xa8, 0x92, 0x80, 0xdd, 0x0e, 0x4d, 0x9e, 0x4e, 0xb1, 0xe3, 0x28, 0x4e, 0xae, 0xd8,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Posição da tabela de deslocamentos FVE, se for BitLocker (normal ou To Go).
|
||||||
|
fn offsets_pos(vbr: &[u8]) -> Option<usize> {
|
||||||
|
if vbr.len() >= 11 && &vbr[3..11] == b"-FVE-FS-" {
|
||||||
|
return Some(176);
|
||||||
|
}
|
||||||
|
// BitLocker To Go: parece FAT ("MSWIN4.1"), mas guarda o GUID do BitLocker no setor de boot.
|
||||||
|
if vbr.len() >= 464 && (vbr[424..440] == GUID_NORMAL || vbr[424..440] == GUID_EOW) {
|
||||||
|
return Some(440);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect(vbr: &[u8]) -> bool {
|
||||||
|
offsets_pos(vbr).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- Elephant
|
||||||
|
|
||||||
|
fn rotl(x: u32, n: u32) -> u32 {
|
||||||
|
x.rotate_left(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RA: [u32; 4] = [9, 0, 13, 0];
|
||||||
|
const RB: [u32; 4] = [0, 10, 0, 25];
|
||||||
|
|
||||||
|
fn words(buf: &[u8]) -> Vec<u32> {
|
||||||
|
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.
|
||||||
|
fn diffuser_a_decrypt(d: &mut [u32]) {
|
||||||
|
let n = d.len();
|
||||||
|
for _ in 0..5 {
|
||||||
|
for i in 0..n {
|
||||||
|
let a = d[(i + n - 2) % n];
|
||||||
|
let b = rotl(d[(i + n - 5) % n], RA[i % 4]);
|
||||||
|
d[i] = d[i].wrapping_add(a ^ b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diffuser_b_decrypt(d: &mut [u32]) {
|
||||||
|
let n = d.len();
|
||||||
|
for _ in 0..3 {
|
||||||
|
for i in 0..n {
|
||||||
|
let a = d[(i + 2) % n];
|
||||||
|
let b = rotl(d[(i + 5) % n], RB[i % 4]);
|
||||||
|
d[i] = d[i].wrapping_add(a ^ b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decifra um setor AES-CBC + difusor Elephant (BitLocker Vista/7 sem XTS).
|
||||||
|
pub fn elephant_decrypt(key: &AesAny, tweak: &AesAny, buf: &mut [u8], byte_off: u64) {
|
||||||
|
let mut iv = [0u8; 16];
|
||||||
|
iv[..8].copy_from_slice(&byte_off.to_le_bytes());
|
||||||
|
key.encrypt(&mut iv);
|
||||||
|
cbc_decrypt(key, &iv, buf);
|
||||||
|
let mut d = words(buf);
|
||||||
|
diffuser_b_decrypt(&mut d);
|
||||||
|
diffuser_a_decrypt(&mut d);
|
||||||
|
for (i, w) in d.iter().enumerate() {
|
||||||
|
buf[i * 4..i * 4 + 4].copy_from_slice(&w.to_le_bytes());
|
||||||
|
}
|
||||||
|
// Chave do setor: duas cifragens do deslocamento com a chave de ajuste.
|
||||||
|
let mut k0 = [0u8; 16];
|
||||||
|
k0[..8].copy_from_slice(&byte_off.to_le_bytes());
|
||||||
|
let mut k1 = k0;
|
||||||
|
k1[15] = 0x80;
|
||||||
|
tweak.encrypt(&mut k0);
|
||||||
|
tweak.encrypt(&mut k1);
|
||||||
|
let mut sk = [0u8; 32];
|
||||||
|
sk[..16].copy_from_slice(&k0);
|
||||||
|
sk[16..].copy_from_slice(&k1);
|
||||||
|
for (i, b) in buf.iter_mut().enumerate() {
|
||||||
|
*b ^= sk[i % 32];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ CCM
|
||||||
|
|
||||||
|
/// AES-CCM (nonce de 12 bytes, tag de 16): `blob` = tag cifrada (16) + dados cifrados.
|
||||||
|
fn ccm_decrypt(key: &AesAny, nonce: &[u8], blob: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
if nonce.len() != 12 || blob.len() < 16 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ctr = |i: u32| -> [u8; 16] {
|
||||||
|
let mut a = [0u8; 16];
|
||||||
|
a[0] = 2;
|
||||||
|
a[1..13].copy_from_slice(nonce);
|
||||||
|
a[13..16].copy_from_slice(&i.to_be_bytes()[1..]);
|
||||||
|
key.encrypt(&mut a);
|
||||||
|
a
|
||||||
|
};
|
||||||
|
let s0 = ctr(0);
|
||||||
|
let mut tag = [0u8; 16];
|
||||||
|
for i in 0..16 {
|
||||||
|
tag[i] = blob[i] ^ s0[i];
|
||||||
|
}
|
||||||
|
let ct = &blob[16..];
|
||||||
|
let mut pt = Vec::with_capacity(ct.len());
|
||||||
|
for (bi, ch) in ct.chunks(16).enumerate() {
|
||||||
|
let s = ctr(bi as u32 + 1);
|
||||||
|
pt.extend(ch.iter().zip(s.iter()).map(|(a, b)| a ^ b));
|
||||||
|
}
|
||||||
|
// CBC-MAC sobre B0 + dados.
|
||||||
|
let mut b0 = [0u8; 16];
|
||||||
|
b0[0] = 0x3A;
|
||||||
|
b0[1..13].copy_from_slice(nonce);
|
||||||
|
b0[13..16].copy_from_slice(&(pt.len() as u32).to_be_bytes()[1..]);
|
||||||
|
key.encrypt(&mut b0);
|
||||||
|
let mut x = b0;
|
||||||
|
for ch in pt.chunks(16) {
|
||||||
|
for i in 0..ch.len() {
|
||||||
|
x[i] ^= ch[i];
|
||||||
|
}
|
||||||
|
key.encrypt(&mut x);
|
||||||
|
}
|
||||||
|
(x == tag).then_some(pt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------- chaves
|
||||||
|
|
||||||
|
fn stretch(initial: &[u8; 32], salt: &[u8]) -> [u8; 32] {
|
||||||
|
let mut last = [0u8; 32];
|
||||||
|
let mut buf = [0u8; 88];
|
||||||
|
buf[32..64].copy_from_slice(initial);
|
||||||
|
buf[64..80].copy_from_slice(salt);
|
||||||
|
for i in 0u64..0x10_0000 {
|
||||||
|
buf[..32].copy_from_slice(&last);
|
||||||
|
buf[80..88].copy_from_slice(&i.to_le_bytes());
|
||||||
|
last.copy_from_slice(&Sha256::digest(buf));
|
||||||
|
}
|
||||||
|
last
|
||||||
|
}
|
||||||
|
|
||||||
|
fn password_key(pw: &str, salt: &[u8]) -> [u8; 32] {
|
||||||
|
let utf16: Vec<u8> = pw.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
|
||||||
|
let h1 = Sha256::digest(&utf16);
|
||||||
|
let mut init = [0u8; 32];
|
||||||
|
init.copy_from_slice(&Sha256::digest(h1));
|
||||||
|
stretch(&init, salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chave de recuperação: 8 blocos de 6 dígitos, cada um múltiplo de 11.
|
||||||
|
fn recovery_key(rp: &str, salt: &[u8]) -> Option<[u8; 32]> {
|
||||||
|
let digits: String = rp.chars().filter(|c| c.is_ascii_digit()).collect();
|
||||||
|
if digits.len() != 48 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut raw = [0u8; 16];
|
||||||
|
for i in 0..8 {
|
||||||
|
let n: u32 = digits[i * 6..i * 6 + 6].parse().ok()?;
|
||||||
|
if n % 11 != 0 || n / 11 > 0xFFFF {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
raw[i * 2..i * 2 + 2].copy_from_slice(&((n / 11) as u16).to_le_bytes());
|
||||||
|
}
|
||||||
|
let mut init = [0u8; 32];
|
||||||
|
init.copy_from_slice(&Sha256::digest(raw));
|
||||||
|
Some(stretch(&init, salt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------- metadados
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Entry {
|
||||||
|
etype: usize,
|
||||||
|
vtype: usize,
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_entries(b: &[u8]) -> Vec<Entry> {
|
||||||
|
let mut v = vec![];
|
||||||
|
let mut o = 0;
|
||||||
|
while o + 8 <= b.len() {
|
||||||
|
let size = le16(b, o);
|
||||||
|
if size < 8 || o + size > b.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
v.push(Entry {
|
||||||
|
etype: le16(b, o + 2),
|
||||||
|
vtype: le16(b, o + 4),
|
||||||
|
data: b[o + 8..o + size].to_vec(),
|
||||||
|
});
|
||||||
|
o += size;
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Vmk {
|
||||||
|
protection: usize,
|
||||||
|
nested: Vec<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vmk {
|
||||||
|
fn ccm(&self) -> Option<&Entry> {
|
||||||
|
self.nested.iter().find(|e| e.vtype == 5)
|
||||||
|
}
|
||||||
|
fn stretch_salt(&self) -> Option<&[u8]> {
|
||||||
|
self.nested
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.vtype == 3)
|
||||||
|
.filter(|e| e.data.len() >= 20)
|
||||||
|
.map(|e| &e.data[4..20])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fve {
|
||||||
|
method: usize,
|
||||||
|
volume_header_offset: u64,
|
||||||
|
volume_header_size: u64,
|
||||||
|
encrypted_size: u64,
|
||||||
|
meta_size: u64,
|
||||||
|
vmks: Vec<Vmk>,
|
||||||
|
fvek: Option<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_fve(src: &dyn Source, offset: u64) -> Option<Fve> {
|
||||||
|
let h = read_vec(src, offset, 64 + 48).ok()?;
|
||||||
|
if h.len() < 112 || &h[..8] != b"-FVE-FS-" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let version = le16(&h, 10);
|
||||||
|
if version != 2 {
|
||||||
|
return None; // Vista (versão 1) não suportado
|
||||||
|
}
|
||||||
|
let encrypted_size = le64(&h, 16);
|
||||||
|
let hdr_sectors = le32(&h, 28) as u64;
|
||||||
|
let volume_header_offset = le64(&h, 56);
|
||||||
|
let meta_size = le32(&h, 64) as usize;
|
||||||
|
if !(48..=1 << 20).contains(&meta_size) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let method = le16(&h, 64 + 36);
|
||||||
|
let all = read_vec(src, offset + 64, meta_size).ok()?;
|
||||||
|
if all.len() < meta_size {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let entries = parse_entries(&all[48..]);
|
||||||
|
let mut vmks = vec![];
|
||||||
|
let mut fvek = None;
|
||||||
|
let (mut vh_off, mut vh_size) = (volume_header_offset, hdr_sectors * 512);
|
||||||
|
for e in entries {
|
||||||
|
match (e.etype, e.vtype) {
|
||||||
|
(15, 15) if e.data.len() >= 16 => {
|
||||||
|
vh_off = le64(&e.data, 0);
|
||||||
|
vh_size = le64(&e.data, 8);
|
||||||
|
}
|
||||||
|
(2, 8) if e.data.len() > 28 => vmks.push(Vmk {
|
||||||
|
protection: le16(&e.data, 26),
|
||||||
|
nested: parse_entries(&e.data[28..]),
|
||||||
|
}),
|
||||||
|
(3, 5) => fvek = Some(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Fve {
|
||||||
|
method,
|
||||||
|
volume_header_offset: vh_off,
|
||||||
|
volume_header_size: vh_size,
|
||||||
|
encrypted_size,
|
||||||
|
meta_size: 0x10000,
|
||||||
|
vmks,
|
||||||
|
fvek,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chave de um arquivo .BEK (chave de inicialização).
|
||||||
|
fn bek_key(file: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
if file.len() < 48 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let es = parse_entries(&file[48..]);
|
||||||
|
for e in &es {
|
||||||
|
if e.vtype == 9 && e.data.len() > 24 {
|
||||||
|
for n in parse_entries(&e.data[24..]) {
|
||||||
|
if n.vtype == 1 && n.data.len() >= 36 {
|
||||||
|
return Some(n.data[4..36].to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate_keys(v: &Vmk, secrets: &[Secret]) -> Vec<Vec<u8>> {
|
||||||
|
let mut out: Vec<Vec<u8>> = vec![];
|
||||||
|
// Chave limpa: o próprio protetor guarda a chave em claro.
|
||||||
|
if v.protection == 0 {
|
||||||
|
for n in &v.nested {
|
||||||
|
if n.vtype == 1 && n.data.len() >= 36 {
|
||||||
|
out.push(n.data[4..36].to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for s in secrets {
|
||||||
|
match s {
|
||||||
|
Secret::Password(p) if v.protection == 0x2000 => {
|
||||||
|
if let Some(salt) = v.stretch_salt() {
|
||||||
|
out.push(password_key(p, salt).to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Secret::RecoveryKey(r) if v.protection == 0x0800 => {
|
||||||
|
if let Some(k) = v.stretch_salt().and_then(|salt| recovery_key(r, salt)) {
|
||||||
|
out.push(k.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Secret::KeyFile(f) if v.protection == 0x0200 => {
|
||||||
|
if let Some(k) = bek_key(f) {
|
||||||
|
out.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_key_entry(key: &[u8], e: &Entry) -> Option<Vec<u8>> {
|
||||||
|
let aes = AesAny::new(key)?;
|
||||||
|
if e.data.len() < 12 + 16 + 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let pt = ccm_decrypt(&aes, &e.data[..12], &e.data[12..])?;
|
||||||
|
// payload = cabeçalho de entrada (8) + método (4) + chave
|
||||||
|
(pt.len() > 12).then(|| pt[12..].to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let vbr = read_vec(src.as_ref(), 0, 512).map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
if !detect(&vbr) || vbr.len() < 512 {
|
||||||
|
return Err(UnlockError::Corrupt("não é BitLocker".into()));
|
||||||
|
}
|
||||||
|
let bps = le16(&vbr, 11).max(512) as u64;
|
||||||
|
let pos = offsets_pos(&vbr).unwrap_or(176);
|
||||||
|
// Três cópias dos metadados; usa a primeira legível.
|
||||||
|
let mut fve = None;
|
||||||
|
let mut meta_offsets = vec![];
|
||||||
|
for i in 0..3 {
|
||||||
|
let off = le64(&vbr, pos + i * 8);
|
||||||
|
if off != 0 {
|
||||||
|
meta_offsets.push(off);
|
||||||
|
}
|
||||||
|
if off != 0 && off < src.size() {
|
||||||
|
if let Some(f) = read_fve(src.as_ref(), off) {
|
||||||
|
fve = Some(f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fve = fve.ok_or_else(|| {
|
||||||
|
UnlockError::Unsupported("metadados FVE ilegíveis ou do Windows Vista".into())
|
||||||
|
})?;
|
||||||
|
let fvek_entry = fve
|
||||||
|
.fvek
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| UnlockError::Corrupt("sem FVEK".into()))?;
|
||||||
|
|
||||||
|
let mut volume_key: Option<Vec<u8>> = None;
|
||||||
|
for s in secrets {
|
||||||
|
if let Secret::VolumeKey(k) = s {
|
||||||
|
volume_key = Some(k.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut vmk_found: Option<Vec<u8>> = None;
|
||||||
|
if volume_key.is_none() {
|
||||||
|
'outer: for v in &fve.vmks {
|
||||||
|
let Some(ccm) = v.ccm() else { continue };
|
||||||
|
for k in candidate_keys(v, secrets) {
|
||||||
|
if let Some(vmk) = decrypt_key_entry(&k, ccm) {
|
||||||
|
if vmk.len() >= 32 {
|
||||||
|
vmk_found = Some(vmk[..32].to_vec());
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fvek_bytes = if let Some(k) = volume_key {
|
||||||
|
k
|
||||||
|
} else {
|
||||||
|
let vmk = vmk_found.ok_or(UnlockError::WrongSecret)?;
|
||||||
|
decrypt_key_entry(&vmk, fvek_entry)
|
||||||
|
.ok_or_else(|| UnlockError::Corrupt("FVEK não decifra com a VMK".into()))?
|
||||||
|
};
|
||||||
|
let k = &fvek_bytes;
|
||||||
|
let bad = || UnlockError::Corrupt("tamanho de chave incompatível com o método".into());
|
||||||
|
let (cipher, name) = match fve.method {
|
||||||
|
0x8000 | 0x8001 => {
|
||||||
|
let n = if fve.method == 0x8000 { 16 } else { 32 };
|
||||||
|
if k.len() < 32 + n {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
(
|
||||||
|
SectorCipher::Elephant {
|
||||||
|
key: AesAny::new(&k[..n]).ok_or_else(bad)?,
|
||||||
|
tweak: AesAny::new(&k[32..32 + n]).ok_or_else(bad)?,
|
||||||
|
},
|
||||||
|
if n == 16 {
|
||||||
|
"AES-128-CBC + difusor"
|
||||||
|
} else {
|
||||||
|
"AES-256-CBC + difusor"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
0x8002 | 0x8003 => {
|
||||||
|
let n = if fve.method == 0x8002 { 16 } else { 32 };
|
||||||
|
if k.len() < n {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
let key = AesAny::new(&k[..n]).ok_or_else(bad)?;
|
||||||
|
(
|
||||||
|
SectorCipher::Cbc {
|
||||||
|
key: key.clone(),
|
||||||
|
iv: Iv::Eboiv(key),
|
||||||
|
},
|
||||||
|
if n == 16 {
|
||||||
|
"AES-128-CBC"
|
||||||
|
} else {
|
||||||
|
"AES-256-CBC"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
0x8004 | 0x8005 => {
|
||||||
|
let n = if fve.method == 0x8004 { 16 } else { 32 };
|
||||||
|
if k.len() < 2 * n {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
let iv = if bps > 512 {
|
||||||
|
Iv::Unit(bps)
|
||||||
|
} else {
|
||||||
|
Iv::Plain64
|
||||||
|
};
|
||||||
|
(
|
||||||
|
SectorCipher::Xts {
|
||||||
|
k1: AesAny::new(&k[..n]).ok_or_else(bad)?,
|
||||||
|
k2: AesAny::new(&k[n..2 * n]).ok_or_else(bad)?,
|
||||||
|
iv,
|
||||||
|
},
|
||||||
|
if n == 16 {
|
||||||
|
"AES-128-XTS"
|
||||||
|
} else {
|
||||||
|
"AES-256-XTS"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
m => {
|
||||||
|
return Err(UnlockError::Unsupported(format!(
|
||||||
|
"método de cifragem BitLocker {m:#x}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let len = src.size();
|
||||||
|
let mut cs = CryptSource::new(src.clone(), cipher, 0, len, bps);
|
||||||
|
let mut patches = vec![];
|
||||||
|
// Áreas de metadados FVE: o Windows não as expõe como dados.
|
||||||
|
for off in meta_offsets {
|
||||||
|
patches.push(Patch {
|
||||||
|
at: off,
|
||||||
|
len: fve.meta_size,
|
||||||
|
from: 0,
|
||||||
|
decrypt_from: None,
|
||||||
|
zero: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if fve.volume_header_size > 0 && fve.volume_header_offset > 0 {
|
||||||
|
// O início do volume original foi movido para outro lugar e cifrado lá.
|
||||||
|
patches.push(Patch {
|
||||||
|
at: fve.volume_header_offset,
|
||||||
|
len: fve.volume_header_size,
|
||||||
|
from: 0,
|
||||||
|
decrypt_from: None,
|
||||||
|
zero: true,
|
||||||
|
});
|
||||||
|
patches.push(Patch {
|
||||||
|
at: 0,
|
||||||
|
len: fve.volume_header_size,
|
||||||
|
from: fve.volume_header_offset,
|
||||||
|
decrypt_from: Some(fve.volume_header_offset),
|
||||||
|
zero: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cs = cs.with_patches(patches);
|
||||||
|
if fve.encrypted_size > 0 && fve.encrypted_size < len {
|
||||||
|
cs = cs.with_plain_from(fve.encrypted_size);
|
||||||
|
}
|
||||||
|
Ok(Unlocked {
|
||||||
|
source: Arc::new(cs),
|
||||||
|
description: format!("BitLocker ({name})"),
|
||||||
|
volume_key: Some(fvek_bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock_window(
|
||||||
|
base: Arc<dyn Source>,
|
||||||
|
start: u64,
|
||||||
|
len: u64,
|
||||||
|
secrets: &[Secret],
|
||||||
|
) -> UnlockResult {
|
||||||
|
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
//! FileVault 2 sobre Core Storage (volumes HFS+ cifrados do macOS 10.7 a 10.12 e Fusion Drives).
|
||||||
|
//! Senha ou chave do volume; chave de recuperação institucional não é suportada.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::source::{read_vec, SubSource};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
const MD_BLOCK: usize = 8192;
|
||||||
|
|
||||||
|
fn le32(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn le64(b: &[u8], o: usize) -> u64 {
|
||||||
|
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect(head: &[u8]) -> bool {
|
||||||
|
head.len() >= 512 && head[88] == 0x43 && head[89] == 0x53 && head[10] == 0x10
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Desembrulha uma chave AES-128 de 24 bytes (RFC 3394).
|
||||||
|
fn unwrap_key(kek: &[u8], wrapped: &[u8]) -> Option<[u8; 16]> {
|
||||||
|
let aes = AesAny::new(kek)?;
|
||||||
|
if wrapped.len() != 24 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut a: [u8; 8] = wrapped[..8].try_into().unwrap();
|
||||||
|
let mut r2: [u8; 8] = wrapped[8..16].try_into().unwrap();
|
||||||
|
let mut r3: [u8; 8] = wrapped[16..24].try_into().unwrap();
|
||||||
|
for t in (1..=12u64).rev() {
|
||||||
|
let prev_r2 = r2;
|
||||||
|
let tb = t.to_be_bytes();
|
||||||
|
let mut blk = [0u8; 16];
|
||||||
|
for i in 0..8 {
|
||||||
|
blk[i] = a[i] ^ tb[i];
|
||||||
|
}
|
||||||
|
blk[8..].copy_from_slice(&r3);
|
||||||
|
aes.decrypt(&mut blk);
|
||||||
|
a.copy_from_slice(&blk[..8]);
|
||||||
|
r2.copy_from_slice(&blk[8..]);
|
||||||
|
r3 = prev_r2;
|
||||||
|
}
|
||||||
|
if a != [0xA6; 8] {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
out[..8].copy_from_slice(&r2);
|
||||||
|
out[8..].copy_from_slice(&r3);
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn b64(s: &str) -> Option<Vec<u8>> {
|
||||||
|
let mut out = vec![];
|
||||||
|
let (mut acc, mut bits) = (0u32, 0);
|
||||||
|
for c in s.bytes() {
|
||||||
|
let v = match c {
|
||||||
|
b'A'..=b'Z' => c - b'A',
|
||||||
|
b'a'..=b'z' => c - b'a' + 26,
|
||||||
|
b'0'..=b'9' => c - b'0' + 52,
|
||||||
|
b'+' => 62,
|
||||||
|
b'/' => 63,
|
||||||
|
b'=' | b'\n' | b'\r' | b' ' | b'\t' => continue,
|
||||||
|
_ => return None,
|
||||||
|
} as u32;
|
||||||
|
acc = (acc << 6) | v;
|
||||||
|
bits += 6;
|
||||||
|
if bits >= 8 {
|
||||||
|
bits -= 8;
|
||||||
|
out.push((acc >> bits) as u8);
|
||||||
|
acc &= (1 << bits) - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valor de `<key>NOME</key><tipo ...>VALOR</tipo>` num plist simples (primeira ocorrência com valor).
|
||||||
|
fn plist_value(xml: &str, key: &str) -> Option<String> {
|
||||||
|
let k = format!("<key>{key}</key>");
|
||||||
|
let mut from = 0;
|
||||||
|
while let Some(p) = xml[from..].find(&k) {
|
||||||
|
let rest = &xml[from + p + k.len()..];
|
||||||
|
from += p + k.len();
|
||||||
|
let Some(open) = rest.find('<').filter(|&o| o == 0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(gt) = rest[open..].find('>') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if rest[open..open + gt].ends_with('/') {
|
||||||
|
continue; // <reference .../>
|
||||||
|
}
|
||||||
|
let Some(te) = rest[open + 1..].find([' ', '>']) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let tag = &rest[open + 1..open + 1 + te];
|
||||||
|
let vstart = open + gt + 1;
|
||||||
|
if let Some(e) = rest[vstart..].find(&format!("</{tag}>")) {
|
||||||
|
let v = rest[vstart..vstart + e].trim();
|
||||||
|
if !v.is_empty() {
|
||||||
|
return Some(v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xml_of(blk: &[u8], off_pos: usize) -> Option<String> {
|
||||||
|
let off = le32(blk, off_pos) as usize;
|
||||||
|
let size = le32(blk, off_pos + 4) as usize;
|
||||||
|
if off >= MD_BLOCK || size > MD_BLOCK - off {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(String::from_utf8_lossy(&blk[off..off + size]).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let corrupt = |s: &str| UnlockError::Corrupt(s.to_string());
|
||||||
|
let h = read_vec(src.as_ref(), 0, 512).map_err(|e| corrupt(&e.to_string()))?;
|
||||||
|
if !detect(&h) {
|
||||||
|
return Err(corrupt("não é um volume Core Storage"));
|
||||||
|
}
|
||||||
|
let block_size = le32(&h, 96) as u64;
|
||||||
|
let disklbl = le64(&h, 104);
|
||||||
|
if block_size == 0 || block_size > 1 << 20 || le32(&h, 168) != 16 {
|
||||||
|
return Err(UnlockError::Unsupported(
|
||||||
|
"Core Storage com chave de metadados que não é AES-128".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut md_key = [0u8; 32];
|
||||||
|
md_key[..16].copy_from_slice(&h[176..192]);
|
||||||
|
md_key[16..].copy_from_slice(&h[304..320]);
|
||||||
|
let (k1, k2) = (
|
||||||
|
AesAny::new(&md_key[..16]).unwrap(),
|
||||||
|
AesAny::new(&md_key[16..]).unwrap(),
|
||||||
|
);
|
||||||
|
let md_cipher = SectorCipher::Xts {
|
||||||
|
k1,
|
||||||
|
k2,
|
||||||
|
iv: Iv::Plain64,
|
||||||
|
};
|
||||||
|
|
||||||
|
let label = read_vec(src.as_ref(), disklbl * block_size, MD_BLOCK)
|
||||||
|
.map_err(|e| corrupt(&e.to_string()))?;
|
||||||
|
if label.len() < MD_BLOCK {
|
||||||
|
return Err(corrupt("rótulo do disco truncado"));
|
||||||
|
}
|
||||||
|
let vgd_off = le32(&label, 220) as u64;
|
||||||
|
let vgd = read_vec(src.as_ref(), disklbl * block_size + vgd_off, 40)
|
||||||
|
.map_err(|e| corrupt(&e.to_string()))?;
|
||||||
|
if vgd.len() < 40 {
|
||||||
|
return Err(corrupt("descritor de grupos truncado"));
|
||||||
|
}
|
||||||
|
let md_blocks = le64(&vgd, 8);
|
||||||
|
let md_start = le64(&vgd, 32) * block_size;
|
||||||
|
|
||||||
|
let (mut iters, mut salt, mut wkek, mut wvk) = (0u32, vec![], vec![], vec![]);
|
||||||
|
let (mut lv_size, mut lv_off) = (0u64, 0u64);
|
||||||
|
let mut family = [0u8; 16];
|
||||||
|
let mut got = 0u8;
|
||||||
|
for i in 0..md_blocks.min(64) {
|
||||||
|
let mut b = read_vec(src.as_ref(), md_start + i * MD_BLOCK as u64, MD_BLOCK)
|
||||||
|
.map_err(|e| corrupt(&e.to_string()))?;
|
||||||
|
if b.len() < MD_BLOCK || b.iter().all(|&x| x == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// XTS sobre o bloco inteiro, com o número do bloco como ajuste inicial.
|
||||||
|
md_cipher.decrypt_sector(&mut b, i, 0);
|
||||||
|
match u16::from_le_bytes([b[10], b[11]]) {
|
||||||
|
0x0019 => {
|
||||||
|
if let Some(xml) = xml_of(&b, 112) {
|
||||||
|
let pwk = plist_value(&xml, "PassphraseWrappedKEKStruct").and_then(|s| b64(&s));
|
||||||
|
let kw = plist_value(&xml, "KEKWrappedVolumeKeyStruct").and_then(|s| b64(&s));
|
||||||
|
if let (Some(p), Some(k)) = (pwk, kw) {
|
||||||
|
if p.len() >= 172 && k.len() >= 32 {
|
||||||
|
salt = p[8..24].to_vec();
|
||||||
|
wkek = p[32..56].to_vec();
|
||||||
|
iters = le32(&p, 168);
|
||||||
|
wvk = k[8..32].to_vec();
|
||||||
|
got |= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x001a => {
|
||||||
|
if let Some(xml) = xml_of(&b, 128) {
|
||||||
|
if let Some(v) = plist_value(&xml, "com.apple.corestorage.lv.size") {
|
||||||
|
lv_size = u64::from_str_radix(v.trim_start_matches("0x"), 16).unwrap_or(0);
|
||||||
|
got |= 8;
|
||||||
|
if let Some(u) = plist_value(&xml, "com.apple.corestorage.lv.familyUUID") {
|
||||||
|
let hex: String = u.chars().filter(|c| c.is_ascii_hexdigit()).collect();
|
||||||
|
if let Some(b) = super::from_hex(&hex).filter(|b| b.len() == 16) {
|
||||||
|
family.copy_from_slice(&b);
|
||||||
|
got |= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x0305 => {
|
||||||
|
lv_off = le32(&b, 104) as u64 * block_size;
|
||||||
|
got |= 4;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got != 7 | 8 {
|
||||||
|
return Err(corrupt("metadados Core Storage incompletos"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let finish = |vk16: [u8; 16]| -> UnlockResult {
|
||||||
|
let mut full = [0u8; 32];
|
||||||
|
full[..16].copy_from_slice(&vk16);
|
||||||
|
full[16..].copy_from_slice(
|
||||||
|
&Sha256::new()
|
||||||
|
.chain_update(vk16)
|
||||||
|
.chain_update(family)
|
||||||
|
.finalize()[..16],
|
||||||
|
);
|
||||||
|
let cipher = SectorCipher::Xts {
|
||||||
|
k1: AesAny::new(&full[..16]).unwrap(),
|
||||||
|
k2: AesAny::new(&full[16..]).unwrap(),
|
||||||
|
iv: Iv::Plain64,
|
||||||
|
};
|
||||||
|
let cs = CryptSource::new(src.clone(), cipher, lv_off, lv_size, 512);
|
||||||
|
Ok(Unlocked {
|
||||||
|
source: Arc::new(cs),
|
||||||
|
description: "FileVault 2 (Core Storage, AES-128-XTS)".into(),
|
||||||
|
volume_key: Some(vk16.to_vec()),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
for s in secrets {
|
||||||
|
if let Secret::VolumeKey(k) = s {
|
||||||
|
if k.len() >= 16 {
|
||||||
|
let mut a = [0u8; 16];
|
||||||
|
a.copy_from_slice(&k[..16]);
|
||||||
|
return finish(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for s in secrets {
|
||||||
|
if let Secret::Password(p) = s {
|
||||||
|
let mut pk = [0u8; 16];
|
||||||
|
pbkdf2::pbkdf2_hmac::<Sha256>(p.as_bytes(), &salt, iters.max(1), &mut pk);
|
||||||
|
if let Some(kek) = unwrap_key(&pk, &wkek) {
|
||||||
|
if let Some(vk) = unwrap_key(&kek, &wvk) {
|
||||||
|
return finish(vk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(UnlockError::WrongSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock_window(
|
||||||
|
base: Arc<dyn Source>,
|
||||||
|
start: u64,
|
||||||
|
len: u64,
|
||||||
|
secrets: &[Secret],
|
||||||
|
) -> UnlockResult {
|
||||||
|
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
||||||
|
}
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
//! LUKS1 e LUKS2 (cifras AES; PBKDF2 e Argon2; hashes SHA-1/256/512 e RIPEMD-160).
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::source::{read_vec, SubSource};
|
||||||
|
use sha1::Sha1;
|
||||||
|
use sha2::{Digest, Sha256, Sha512};
|
||||||
|
|
||||||
|
pub const MAGIC: &[u8; 6] = b"LUKS\xBA\xBE";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Hash {
|
||||||
|
Sha1,
|
||||||
|
Sha256,
|
||||||
|
Sha512,
|
||||||
|
Ripemd160,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash {
|
||||||
|
pub fn parse(s: &str) -> Option<Hash> {
|
||||||
|
Some(match s.to_ascii_lowercase().as_str() {
|
||||||
|
"sha1" => Hash::Sha1,
|
||||||
|
"sha256" => Hash::Sha256,
|
||||||
|
"sha512" => Hash::Sha512,
|
||||||
|
"ripemd160" | "rmd160" => Hash::Ripemd160,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn size(self) -> usize {
|
||||||
|
match self {
|
||||||
|
Hash::Sha1 | Hash::Ripemd160 => 20,
|
||||||
|
Hash::Sha256 => 32,
|
||||||
|
Hash::Sha512 => 64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn digest(self, parts: &[&[u8]]) -> Vec<u8> {
|
||||||
|
fn run<D: Digest>(parts: &[&[u8]]) -> Vec<u8> {
|
||||||
|
let mut d = D::new();
|
||||||
|
for p in parts {
|
||||||
|
d.update(p);
|
||||||
|
}
|
||||||
|
d.finalize().to_vec()
|
||||||
|
}
|
||||||
|
match self {
|
||||||
|
Hash::Sha1 => run::<Sha1>(parts),
|
||||||
|
Hash::Sha256 => run::<Sha256>(parts),
|
||||||
|
Hash::Sha512 => run::<Sha512>(parts),
|
||||||
|
Hash::Ripemd160 => run::<ripemd::Ripemd160>(parts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn pbkdf2(self, pass: &[u8], salt: &[u8], iterations: u32, len: usize) -> Vec<u8> {
|
||||||
|
let mut out = vec![0u8; len];
|
||||||
|
let it = iterations.max(1);
|
||||||
|
match self {
|
||||||
|
Hash::Sha1 => pbkdf2::pbkdf2_hmac::<Sha1>(pass, salt, it, &mut out),
|
||||||
|
Hash::Sha256 => pbkdf2::pbkdf2_hmac::<Sha256>(pass, salt, it, &mut out),
|
||||||
|
Hash::Sha512 => pbkdf2::pbkdf2_hmac::<Sha512>(pass, salt, it, &mut out),
|
||||||
|
Hash::Ripemd160 => pbkdf2::pbkdf2_hmac::<ripemd::Ripemd160>(pass, salt, it, &mut out),
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Difusão do "anti-forensic splitter".
|
||||||
|
fn diffuse(block: &mut [u8], h: Hash) {
|
||||||
|
let ds = h.size();
|
||||||
|
let mut i = 0u32;
|
||||||
|
let mut off = 0;
|
||||||
|
while off < block.len() {
|
||||||
|
let n = ds.min(block.len() - off);
|
||||||
|
let d = h.digest(&[&i.to_be_bytes(), &block[off..off + n]]);
|
||||||
|
block[off..off + n].copy_from_slice(&d[..n]);
|
||||||
|
off += n;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompõe a chave a partir das `stripes` faixas (AFmerge).
|
||||||
|
pub fn af_merge(src: &[u8], block: usize, stripes: usize, h: Hash) -> Vec<u8> {
|
||||||
|
let mut buf = vec![0u8; block];
|
||||||
|
for i in 0..stripes.saturating_sub(1) {
|
||||||
|
for (b, s) in buf.iter_mut().zip(&src[i * block..(i + 1) * block]) {
|
||||||
|
*b ^= *s;
|
||||||
|
}
|
||||||
|
diffuse(&mut buf, h);
|
||||||
|
}
|
||||||
|
let last = &src[(stripes - 1) * block..stripes * block];
|
||||||
|
buf.iter().zip(last).map(|(a, b)| a ^ b).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
let (alg, rest) = spec
|
||||||
|
.split_once('-')
|
||||||
|
.ok_or_else(|| UnlockError::Unsupported(spec.into()))?;
|
||||||
|
if !alg.eq_ignore_ascii_case("aes") {
|
||||||
|
return Err(UnlockError::Unsupported(format!(
|
||||||
|
"cifra {alg} (apenas AES)"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let (mode, ivs) = rest.split_once('-').unwrap_or((rest, "plain"));
|
||||||
|
let bad = || UnlockError::Unsupported(format!("chave inválida para {spec}"));
|
||||||
|
let mk_iv = |k: &[u8]| -> std::result::Result<Iv, UnlockError> {
|
||||||
|
let ivl = ivs.to_ascii_lowercase();
|
||||||
|
Ok(match ivl.as_str() {
|
||||||
|
"plain64" => Iv::Plain64,
|
||||||
|
"plain" | "plain32" => Iv::Plain32,
|
||||||
|
"plain64be" => Iv::Plain64Be,
|
||||||
|
s if s.starts_with("essiv:") => {
|
||||||
|
let h =
|
||||||
|
Hash::parse(&s[6..]).ok_or_else(|| UnlockError::Unsupported(spec.into()))?;
|
||||||
|
Iv::Essiv(AesAny::new(&h.digest(&[k])).ok_or_else(bad)?)
|
||||||
|
}
|
||||||
|
_ => return Err(UnlockError::Unsupported(format!("IV {ivs}"))),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
match mode.to_ascii_lowercase().as_str() {
|
||||||
|
"xts" => {
|
||||||
|
if key.len() % 2 != 0 {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
let h = key.len() / 2;
|
||||||
|
Ok(SectorCipher::Xts {
|
||||||
|
k1: AesAny::new(&key[..h]).ok_or_else(bad)?,
|
||||||
|
k2: AesAny::new(&key[h..]).ok_or_else(bad)?,
|
||||||
|
iv: mk_iv(&key[..h])?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"cbc" => Ok(SectorCipher::Cbc {
|
||||||
|
key: AesAny::new(key).ok_or_else(bad)?,
|
||||||
|
iv: mk_iv(key)?,
|
||||||
|
}),
|
||||||
|
m => Err(UnlockError::Unsupported(format!("modo {m}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn passwords(secrets: &[Secret]) -> Vec<Vec<u8>> {
|
||||||
|
secrets
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| match s {
|
||||||
|
Secret::Password(p) => Some(p.as_bytes().to_vec()),
|
||||||
|
Secret::KeyFile(k) => Some(k.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn volume_keys(secrets: &[Secret]) -> Vec<Vec<u8>> {
|
||||||
|
secrets
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| match s {
|
||||||
|
Secret::VolumeKey(k) => Some(k.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect(head: &[u8]) -> bool {
|
||||||
|
head.len() >= 8 && &head[..6] == MAGIC
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let head = read_vec(src.as_ref(), 0, 8).map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
if !detect(&head) {
|
||||||
|
return Err(UnlockError::Corrupt("não é LUKS".into()));
|
||||||
|
}
|
||||||
|
match u16::from_be_bytes([head[6], head[7]]) {
|
||||||
|
1 => unlock_v1(src, secrets),
|
||||||
|
2 => unlock_v2(src, secrets),
|
||||||
|
v => Err(UnlockError::Unsupported(format!("LUKS versão {v}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn be32(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_be_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cstr(b: &[u8]) -> String {
|
||||||
|
let n = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||||||
|
String::from_utf8_lossy(&b[..n]).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock_v1(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let h = read_vec(src.as_ref(), 0, 592).map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
if h.len() < 592 {
|
||||||
|
return Err(UnlockError::Corrupt("cabeçalho LUKS1 truncado".into()));
|
||||||
|
}
|
||||||
|
let cipher_name = cstr(&h[8..40]);
|
||||||
|
let cipher_mode = cstr(&h[40..72]);
|
||||||
|
let hash = Hash::parse(&cstr(&h[72..104]))
|
||||||
|
.ok_or_else(|| UnlockError::Unsupported(format!("hash {}", cstr(&h[72..104]))))?;
|
||||||
|
let payload = be32(&h, 104) as u64 * 512;
|
||||||
|
let key_bytes = be32(&h, 108) as usize;
|
||||||
|
let mk_digest = &h[112..132];
|
||||||
|
let mk_salt = &h[132..164];
|
||||||
|
let mk_iter = be32(&h, 164);
|
||||||
|
let spec = format!("{cipher_name}-{cipher_mode}");
|
||||||
|
if key_bytes == 0 || key_bytes > 128 {
|
||||||
|
return Err(UnlockError::Corrupt("tamanho de chave inválido".into()));
|
||||||
|
}
|
||||||
|
let check = |mk: &[u8]| hash.pbkdf2(mk, mk_salt, mk_iter, 20) == mk_digest;
|
||||||
|
let finish = |mk: Vec<u8>| -> UnlockResult {
|
||||||
|
let cipher = parse_cipher(&spec, &mk)?;
|
||||||
|
let len = src.size().saturating_sub(payload);
|
||||||
|
let cs = CryptSource::new(src.clone(), cipher, payload, len, 512);
|
||||||
|
Ok(Unlocked {
|
||||||
|
source: Arc::new(cs),
|
||||||
|
description: format!("LUKS1 ({spec})"),
|
||||||
|
volume_key: Some(mk),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
for vk in volume_keys(secrets) {
|
||||||
|
if vk.len() == key_bytes && check(&vk) {
|
||||||
|
return finish(vk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let pws = passwords(secrets);
|
||||||
|
for slot in 0..8 {
|
||||||
|
let o = 208 + slot * 48;
|
||||||
|
if be32(&h, o) != 0x00AC_71F3 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let iter = be32(&h, o + 4);
|
||||||
|
let salt = &h[o + 8..o + 40];
|
||||||
|
let km_off = be32(&h, o + 40) as u64 * 512;
|
||||||
|
let stripes = be32(&h, o + 44) as usize;
|
||||||
|
if stripes == 0 || stripes > 100_000 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let km_len = (key_bytes * stripes).div_ceil(512) * 512;
|
||||||
|
let Ok(mut km) = read_vec(src.as_ref(), km_off, km_len) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if km.len() < km_len {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for pw in &pws {
|
||||||
|
let dk = hash.pbkdf2(pw, salt, iter, key_bytes);
|
||||||
|
let cipher = parse_cipher(&spec, &dk)?;
|
||||||
|
let mut m = km.clone();
|
||||||
|
for (i, s) in m.chunks_exact_mut(512).enumerate() {
|
||||||
|
cipher.decrypt_sector(s, i as u64, i as u64 * 512);
|
||||||
|
}
|
||||||
|
let mk = af_merge(&m, key_bytes, stripes, hash);
|
||||||
|
if check(&mk) {
|
||||||
|
return finish(mk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
km.clear();
|
||||||
|
}
|
||||||
|
Err(UnlockError::WrongSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn b64(s: &str) -> Option<Vec<u8>> {
|
||||||
|
let mut out = vec![];
|
||||||
|
let mut acc = 0u32;
|
||||||
|
let mut bits = 0;
|
||||||
|
for c in s.bytes() {
|
||||||
|
let v = match c {
|
||||||
|
b'A'..=b'Z' => c - b'A',
|
||||||
|
b'a'..=b'z' => c - b'a' + 26,
|
||||||
|
b'0'..=b'9' => c - b'0' + 52,
|
||||||
|
b'+' => 62,
|
||||||
|
b'/' => 63,
|
||||||
|
b'=' | b'\n' | b'\r' => continue,
|
||||||
|
_ => return None,
|
||||||
|
} as u32;
|
||||||
|
acc = (acc << 6) | v;
|
||||||
|
bits += 6;
|
||||||
|
if bits >= 8 {
|
||||||
|
bits -= 8;
|
||||||
|
out.push((acc >> bits) as u8);
|
||||||
|
acc &= (1 << bits) - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jstr<'a>(v: &'a serde_json::Value, k: &str) -> Option<&'a str> {
|
||||||
|
v.get(k)?.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Números no JSON do LUKS2 vêm como texto (inteiros grandes) ou número.
|
||||||
|
fn jnum(v: &serde_json::Value, k: &str) -> Option<u64> {
|
||||||
|
let x = v.get(k)?;
|
||||||
|
x.as_u64().or_else(|| x.as_str()?.parse().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let h = read_vec(src.as_ref(), 0, 4096).map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
if h.len() < 4096 {
|
||||||
|
return Err(UnlockError::Corrupt("cabeçalho LUKS2 truncado".into()));
|
||||||
|
}
|
||||||
|
let hdr_size = u64::from_be_bytes(h[8..16].try_into().unwrap());
|
||||||
|
if !(16384..=(4 << 20)).contains(&hdr_size) {
|
||||||
|
return Err(UnlockError::Corrupt(
|
||||||
|
"tamanho de cabeçalho LUKS2 inválido".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let area = read_vec(src.as_ref(), 4096, (hdr_size - 4096) as usize)
|
||||||
|
.map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
let end = area.iter().position(|&c| c == 0).unwrap_or(area.len());
|
||||||
|
let json: serde_json::Value = serde_json::from_slice(&area[..end])
|
||||||
|
.map_err(|e| UnlockError::Corrupt(format!("JSON: {e}")))?;
|
||||||
|
let segments = json
|
||||||
|
.get("segments")
|
||||||
|
.and_then(|s| s.as_object())
|
||||||
|
.ok_or_else(|| UnlockError::Corrupt("sem segments".into()))?;
|
||||||
|
let (_, seg) = segments
|
||||||
|
.iter()
|
||||||
|
.find(|(_, s)| jstr(s, "type") == Some("crypt"))
|
||||||
|
.ok_or_else(|| UnlockError::Unsupported("sem segmento cifrado".into()))?;
|
||||||
|
let seg_off =
|
||||||
|
jnum(seg, "offset").ok_or_else(|| UnlockError::Corrupt("offset do segmento".into()))?;
|
||||||
|
let seg_len = match seg.get("size").and_then(|s| s.as_str()) {
|
||||||
|
Some("dynamic") | None => src.size().saturating_sub(seg_off),
|
||||||
|
Some(n) => n.parse().unwrap_or(0),
|
||||||
|
};
|
||||||
|
let seg_enc = jstr(seg, "encryption")
|
||||||
|
.unwrap_or("aes-xts-plain64")
|
||||||
|
.to_string();
|
||||||
|
let sector = jnum(seg, "sector_size").unwrap_or(512);
|
||||||
|
let iv_tweak = jnum(seg, "iv_tweak").unwrap_or(0);
|
||||||
|
let digests = json
|
||||||
|
.get("digests")
|
||||||
|
.and_then(|d| d.as_object())
|
||||||
|
.ok_or_else(|| UnlockError::Corrupt("sem digests".into()))?;
|
||||||
|
|
||||||
|
let check = |vk: &[u8]| -> bool {
|
||||||
|
for (_, d) in digests {
|
||||||
|
if jstr(d, "type") != Some("pbkdf2") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (Some(hash), Some(salt), Some(want), Some(it)) = (
|
||||||
|
jstr(d, "hash").and_then(Hash::parse),
|
||||||
|
jstr(d, "salt").and_then(b64),
|
||||||
|
jstr(d, "digest").and_then(b64),
|
||||||
|
jnum(d, "iterations"),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if hash.pbkdf2(vk, &salt, it as u32, want.len()) == want {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
};
|
||||||
|
let finish = |vk: Vec<u8>| -> UnlockResult {
|
||||||
|
let cipher = parse_cipher(&seg_enc, &vk)?;
|
||||||
|
let cs =
|
||||||
|
CryptSource::new(src.clone(), cipher, seg_off, seg_len, sector).with_iv_tweak(iv_tweak);
|
||||||
|
Ok(Unlocked {
|
||||||
|
source: Arc::new(cs),
|
||||||
|
description: format!("LUKS2 ({seg_enc})"),
|
||||||
|
volume_key: Some(vk),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
for vk in volume_keys(secrets) {
|
||||||
|
if check(&vk) {
|
||||||
|
return finish(vk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let pws = passwords(secrets);
|
||||||
|
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;
|
||||||
|
for (_, slot) in slots {
|
||||||
|
if jstr(slot, "type") != Some("luks2") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let key_size = jnum(slot, "key_size").unwrap_or(0) as usize;
|
||||||
|
let af = slot.get("af");
|
||||||
|
let stripes = af.and_then(|a| jnum(a, "stripes")).unwrap_or(4000) as usize;
|
||||||
|
let af_hash = af
|
||||||
|
.and_then(|a| jstr(a, "hash"))
|
||||||
|
.and_then(Hash::parse)
|
||||||
|
.unwrap_or(Hash::Sha256);
|
||||||
|
let ar = slot.get("area");
|
||||||
|
let (Some(a_off), Some(a_size)) = (
|
||||||
|
ar.and_then(|a| jnum(a, "offset")),
|
||||||
|
ar.and_then(|a| jnum(a, "size")),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let a_enc = ar
|
||||||
|
.and_then(|a| jstr(a, "encryption"))
|
||||||
|
.unwrap_or("aes-xts-plain64");
|
||||||
|
let a_ksize = ar.and_then(|a| jnum(a, "key_size")).unwrap_or(64) as usize;
|
||||||
|
let kdf = slot.get("kdf");
|
||||||
|
let Some(kdf) = kdf else { continue };
|
||||||
|
let salt = jstr(kdf, "salt").and_then(b64).unwrap_or_default();
|
||||||
|
let km_len = (key_size * stripes).div_ceil(512) * 512;
|
||||||
|
if key_size == 0 || stripes == 0 || km_len as u64 > a_size {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(km) = read_vec(src.as_ref(), a_off, km_len) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if km.len() < km_len {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for pw in &pws {
|
||||||
|
let dk: Vec<u8> = match jstr(kdf, "type") {
|
||||||
|
Some("pbkdf2") => {
|
||||||
|
let hh = jstr(kdf, "hash")
|
||||||
|
.and_then(Hash::parse)
|
||||||
|
.unwrap_or(Hash::Sha256);
|
||||||
|
hh.pbkdf2(
|
||||||
|
pw,
|
||||||
|
&salt,
|
||||||
|
jnum(kdf, "iterations").unwrap_or(1000) as u32,
|
||||||
|
a_ksize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Some(t @ ("argon2i" | "argon2id")) => {
|
||||||
|
let alg = if t == "argon2i" {
|
||||||
|
argon2::Algorithm::Argon2i
|
||||||
|
} else {
|
||||||
|
argon2::Algorithm::Argon2id
|
||||||
|
};
|
||||||
|
let params = match argon2::Params::new(
|
||||||
|
jnum(kdf, "memory").unwrap_or(65536) as u32,
|
||||||
|
jnum(kdf, "time").unwrap_or(4) as u32,
|
||||||
|
jnum(kdf, "cpus").unwrap_or(1) as u32,
|
||||||
|
Some(a_ksize),
|
||||||
|
) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
unsupported = Some(format!("parâmetros Argon2: {e}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
Some(t) => {
|
||||||
|
unsupported = Some(format!("KDF {t}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let cipher = parse_cipher(a_enc, &dk)?;
|
||||||
|
let mut m = km.clone();
|
||||||
|
for (i, s) in m.chunks_exact_mut(512).enumerate() {
|
||||||
|
cipher.decrypt_sector(s, i as u64, i as u64 * 512);
|
||||||
|
}
|
||||||
|
let vk = af_merge(&m, key_size, stripes, af_hash);
|
||||||
|
if check(&vk) {
|
||||||
|
return finish(vk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match unsupported {
|
||||||
|
Some(u) => Err(UnlockError::Unsupported(u)),
|
||||||
|
None => Err(UnlockError::WrongSecret),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atalho: abre uma janela da origem como LUKS.
|
||||||
|
pub fn unlock_window(
|
||||||
|
base: Arc<dyn Source>,
|
||||||
|
start: u64,
|
||||||
|
len: u64,
|
||||||
|
secrets: &[Secret],
|
||||||
|
) -> UnlockResult {
|
||||||
|
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
//! Cifras de disco (somente leitura) e desbloqueio de volumes cifrados.
|
||||||
|
//!
|
||||||
|
//! O volume desbloqueado é exposto como uma `Source` comum: o restante do Guará
|
||||||
|
//! (varredura, recuperação) enxerga o sistema de arquivos em claro, sem saber
|
||||||
|
//! que existe cifra por baixo.
|
||||||
|
|
||||||
|
pub mod bitlocker;
|
||||||
|
pub mod filevault;
|
||||||
|
pub mod luks;
|
||||||
|
|
||||||
|
use crate::source::Source;
|
||||||
|
use aes::cipher::generic_array::GenericArray;
|
||||||
|
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
|
||||||
|
use aes::{Aes128, Aes192, Aes256};
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// AES com tamanho de chave escolhido em tempo de execução.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum AesAny {
|
||||||
|
A128(Aes128),
|
||||||
|
A192(Aes192),
|
||||||
|
A256(Aes256),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AesAny {
|
||||||
|
pub fn new(key: &[u8]) -> Option<Self> {
|
||||||
|
Some(match key.len() {
|
||||||
|
16 => AesAny::A128(Aes128::new_from_slice(key).ok()?),
|
||||||
|
24 => AesAny::A192(Aes192::new_from_slice(key).ok()?),
|
||||||
|
32 => AesAny::A256(Aes256::new_from_slice(key).ok()?),
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn encrypt(&self, b: &mut [u8; 16]) {
|
||||||
|
let g = GenericArray::from_mut_slice(b);
|
||||||
|
match self {
|
||||||
|
AesAny::A128(c) => c.encrypt_block(g),
|
||||||
|
AesAny::A192(c) => c.encrypt_block(g),
|
||||||
|
AesAny::A256(c) => c.encrypt_block(g),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn decrypt(&self, b: &mut [u8; 16]) {
|
||||||
|
let g = GenericArray::from_mut_slice(b);
|
||||||
|
match self {
|
||||||
|
AesAny::A128(c) => c.decrypt_block(g),
|
||||||
|
AesAny::A192(c) => c.decrypt_block(g),
|
||||||
|
AesAny::A256(c) => c.decrypt_block(g),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xor16(a: &mut [u8; 16], b: &[u8]) {
|
||||||
|
for i in 0..16 {
|
||||||
|
a[i] ^= b[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Como o vetor de inicialização de cada setor é calculado.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Iv {
|
||||||
|
/// Número do setor (de 512 bytes) em 64 bits little-endian.
|
||||||
|
Plain64,
|
||||||
|
/// Número do setor em 32 bits little-endian.
|
||||||
|
Plain32,
|
||||||
|
Plain64Be,
|
||||||
|
/// ESSIV: AES(SHA-256(chave), setor).
|
||||||
|
Essiv(AesAny),
|
||||||
|
/// Número da unidade de dados (setor de `usize` bytes) em 64 bits little-endian.
|
||||||
|
Unit(u64),
|
||||||
|
/// BitLocker (Windows 10 1511+): AES(chave, offset em bytes).
|
||||||
|
Eboiv(AesAny),
|
||||||
|
/// BitLocker CBC com difusor Elephant: IV = AES(chave, offset em bytes).
|
||||||
|
Bitlocker(AesAny),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iv {
|
||||||
|
fn make(&self, sector_512: u64, byte_off: u64) -> [u8; 16] {
|
||||||
|
let mut iv = [0u8; 16];
|
||||||
|
match self {
|
||||||
|
Iv::Plain64 => iv[..8].copy_from_slice(§or_512.to_le_bytes()),
|
||||||
|
Iv::Plain32 => iv[..4].copy_from_slice(&(sector_512 as u32).to_le_bytes()),
|
||||||
|
Iv::Plain64Be => iv[8..].copy_from_slice(§or_512.to_be_bytes()),
|
||||||
|
Iv::Unit(u) => iv[..8].copy_from_slice(&(byte_off / u).to_le_bytes()),
|
||||||
|
Iv::Essiv(k) => {
|
||||||
|
iv[..8].copy_from_slice(§or_512.to_le_bytes());
|
||||||
|
k.encrypt(&mut iv);
|
||||||
|
}
|
||||||
|
Iv::Eboiv(k) | Iv::Bitlocker(k) => {
|
||||||
|
iv[..8].copy_from_slice(&byte_off.to_le_bytes());
|
||||||
|
k.encrypt(&mut iv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cifra aplicada setor a setor.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SectorCipher {
|
||||||
|
Xts {
|
||||||
|
k1: AesAny,
|
||||||
|
k2: AesAny,
|
||||||
|
iv: Iv,
|
||||||
|
},
|
||||||
|
Cbc {
|
||||||
|
key: AesAny,
|
||||||
|
iv: Iv,
|
||||||
|
},
|
||||||
|
/// BitLocker AES-CBC com difusor Elephant (chave de dados + chave de ajuste).
|
||||||
|
Elephant {
|
||||||
|
key: AesAny,
|
||||||
|
tweak: AesAny,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gf_double(t: &mut [u8; 16]) {
|
||||||
|
let mut carry = 0u8;
|
||||||
|
for b in t.iter_mut() {
|
||||||
|
let next = *b >> 7;
|
||||||
|
*b = (*b << 1) | carry;
|
||||||
|
carry = next;
|
||||||
|
}
|
||||||
|
if carry != 0 {
|
||||||
|
t[0] ^= 0x87;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SectorCipher {
|
||||||
|
/// Decifra um setor no lugar. `sector_512` conta setores de 512 bytes; `byte_off` é a posição
|
||||||
|
/// em bytes relativa ao início dos dados cifrados.
|
||||||
|
pub fn decrypt_sector(&self, buf: &mut [u8], sector_512: u64, byte_off: u64) {
|
||||||
|
match self {
|
||||||
|
SectorCipher::Xts { k1, k2, iv } => {
|
||||||
|
let mut t = iv.make(sector_512, byte_off);
|
||||||
|
k2.encrypt(&mut t);
|
||||||
|
for ch in buf.chunks_exact_mut(16) {
|
||||||
|
let mut b = [0u8; 16];
|
||||||
|
b.copy_from_slice(ch);
|
||||||
|
xor16(&mut b, &t);
|
||||||
|
k1.decrypt(&mut b);
|
||||||
|
xor16(&mut b, &t);
|
||||||
|
ch.copy_from_slice(&b);
|
||||||
|
gf_double(&mut t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SectorCipher::Cbc { key, iv } => {
|
||||||
|
cbc_decrypt(key, &iv.make(sector_512, byte_off), buf);
|
||||||
|
}
|
||||||
|
SectorCipher::Elephant { key, tweak } => {
|
||||||
|
bitlocker::elephant_decrypt(key, tweak, buf, byte_off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cbc_decrypt(key: &AesAny, iv: &[u8; 16], buf: &mut [u8]) {
|
||||||
|
let mut prev = *iv;
|
||||||
|
for ch in buf.chunks_exact_mut(16) {
|
||||||
|
let mut c = [0u8; 16];
|
||||||
|
c.copy_from_slice(ch);
|
||||||
|
let mut b = c;
|
||||||
|
key.decrypt(&mut b);
|
||||||
|
xor16(&mut b, &prev);
|
||||||
|
ch.copy_from_slice(&b);
|
||||||
|
prev = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cbc_encrypt(key: &AesAny, iv: &[u8; 16], buf: &mut [u8]) {
|
||||||
|
let mut prev = *iv;
|
||||||
|
for ch in buf.chunks_exact_mut(16) {
|
||||||
|
let mut b = [0u8; 16];
|
||||||
|
b.copy_from_slice(ch);
|
||||||
|
xor16(&mut b, &prev);
|
||||||
|
key.encrypt(&mut b);
|
||||||
|
ch.copy_from_slice(&b);
|
||||||
|
prev = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Volume cifrado visto em claro: `len` bytes que começam em `data_offset` na origem.
|
||||||
|
pub struct CryptSource {
|
||||||
|
inner: Arc<dyn Source>,
|
||||||
|
cipher: SectorCipher,
|
||||||
|
data_offset: u64,
|
||||||
|
len: u64,
|
||||||
|
sector: u64,
|
||||||
|
/// Setores antes deste limite estão em claro (BitLocker parcialmente cifrado) ou
|
||||||
|
/// são cobertos por uma cópia (os primeiros setores do BitLocker).
|
||||||
|
patches: Vec<Patch>,
|
||||||
|
/// A partir deste byte (relativo aos dados) o conteúdo ainda não foi cifrado.
|
||||||
|
plain_from: u64,
|
||||||
|
/// Somado ao número do setor no cálculo do IV (iv_tweak do LUKS2).
|
||||||
|
iv_tweak: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trecho do volume lido de outro lugar da origem (BitLocker move o início do volume).
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Patch {
|
||||||
|
/// Início no volume em claro.
|
||||||
|
pub at: u64,
|
||||||
|
pub len: u64,
|
||||||
|
/// Onde estão os dados, na origem, já em claro.
|
||||||
|
pub from: u64,
|
||||||
|
/// Se os bytes de origem precisam ser decifrados (usando a posição `from_crypt`).
|
||||||
|
pub decrypt_from: Option<u64>,
|
||||||
|
/// Devolve zeros (áreas de metadados do BitLocker).
|
||||||
|
pub zero: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CryptSource {
|
||||||
|
pub fn new(
|
||||||
|
inner: Arc<dyn Source>,
|
||||||
|
cipher: SectorCipher,
|
||||||
|
data_offset: u64,
|
||||||
|
len: u64,
|
||||||
|
sector: u64,
|
||||||
|
) -> Self {
|
||||||
|
let sector = sector.max(512);
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
cipher,
|
||||||
|
data_offset,
|
||||||
|
len: len / sector * sector,
|
||||||
|
sector,
|
||||||
|
patches: vec![],
|
||||||
|
plain_from: u64::MAX,
|
||||||
|
iv_tweak: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_patches(mut self, p: Vec<Patch>) -> Self {
|
||||||
|
self.patches = p;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn with_iv_tweak(mut self, t: u64) -> Self {
|
||||||
|
self.iv_tweak = t;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn with_plain_from(mut self, off: u64) -> Self {
|
||||||
|
self.plain_from = off;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_sector_plain(&self, rel: u64, out: &mut [u8]) -> io::Result<()> {
|
||||||
|
// `rel` alinhado ao setor; `out.len() == sector`.
|
||||||
|
for p in &self.patches {
|
||||||
|
if rel >= p.at && rel < p.at + p.len {
|
||||||
|
if p.zero {
|
||||||
|
out.fill(0);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let src_off = p.from + (rel - p.at);
|
||||||
|
crate::source::read_exact_at(self.inner.as_ref(), self.data_offset + src_off, out)?;
|
||||||
|
if let Some(pos) = p.decrypt_from {
|
||||||
|
let r = pos + (rel - p.at);
|
||||||
|
self.cipher.decrypt_sector(out, r / 512, r);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::source::read_exact_at(self.inner.as_ref(), self.data_offset + rel, out)?;
|
||||||
|
if rel < self.plain_from {
|
||||||
|
self.cipher
|
||||||
|
.decrypt_sector(out, rel / 512 + self.iv_tweak, rel);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for CryptSource {
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.len || buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let want = buf.len().min((self.len - offset) as usize);
|
||||||
|
let s = self.sector;
|
||||||
|
let mut done = 0usize;
|
||||||
|
let mut tmp = vec![0u8; s as usize];
|
||||||
|
while done < want {
|
||||||
|
let pos = offset + done as u64;
|
||||||
|
let rel = pos / s * s;
|
||||||
|
let within = (pos - rel) as usize;
|
||||||
|
let n = (s as usize - within).min(want - done);
|
||||||
|
self.read_sector_plain(rel, &mut tmp)?;
|
||||||
|
buf[done..done + n].copy_from_slice(&tmp[within..within + n]);
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Algo que o usuário fornece para abrir um volume.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Secret {
|
||||||
|
Password(String),
|
||||||
|
/// Chave de recuperação numérica do BitLocker (8 grupos de 6 dígitos).
|
||||||
|
RecoveryKey(String),
|
||||||
|
/// Conteúdo de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS).
|
||||||
|
KeyFile(Vec<u8>),
|
||||||
|
/// Chave do volume já conhecida (FVEK/master key).
|
||||||
|
VolumeKey(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Secret {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Secret::Password(_) => "Secret::Password(..)",
|
||||||
|
Secret::RecoveryKey(_) => "Secret::RecoveryKey(..)",
|
||||||
|
Secret::KeyFile(_) => "Secret::KeyFile(..)",
|
||||||
|
Secret::VolumeKey(_) => "Secret::VolumeKey(..)",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum UnlockError {
|
||||||
|
/// Nenhuma das senhas/chaves abriu o volume.
|
||||||
|
WrongSecret,
|
||||||
|
Unsupported(String),
|
||||||
|
Corrupt(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for UnlockError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
UnlockError::WrongSecret => write!(f, "senha ou chave incorreta"),
|
||||||
|
UnlockError::Unsupported(s) => write!(f, "não suportado: {s}"),
|
||||||
|
UnlockError::Corrupt(s) => write!(f, "cabeçalho inválido: {s}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UnlockError> for crate::Error {
|
||||||
|
fn from(e: UnlockError) -> Self {
|
||||||
|
crate::Error::msg(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Volume aberto.
|
||||||
|
pub struct Unlocked {
|
||||||
|
pub source: Arc<dyn Source>,
|
||||||
|
pub description: String,
|
||||||
|
/// Chave do volume (para reabrir sem a senha, se o usuário permitir guardá-la).
|
||||||
|
pub volume_key: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type UnlockResult = std::result::Result<Unlocked, UnlockError>;
|
||||||
|
|
||||||
|
pub fn to_hex(b: &[u8]) -> String {
|
||||||
|
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_hex(s: &str) -> Option<Vec<u8>> {
|
||||||
|
let s = s.trim();
|
||||||
|
if s.len() % 2 != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
(0..s.len() / 2)
|
||||||
|
.map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
//! Recomposição de arquivos fragmentados no carving.
|
||||||
|
//!
|
||||||
|
//! Para JPEG: valida os bytes contíguos com o decodificador de entropia; se a imagem quebra,
|
||||||
|
//! procura entre os clusters "com cara de JPEG" próximos a continuação cujo DC de luminância
|
||||||
|
//! emenda sem salto, até três fragmentos. É o que dá para fazer sem tabela de alocação, e só
|
||||||
|
//! vale quando o sistema de arquivos não deixou metadados para o arquivo.
|
||||||
|
|
||||||
|
use crate::jpegscan::{self, Header, State, Stop};
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use crate::types::Extent;
|
||||||
|
|
||||||
|
const MAX_FILE: usize = 48 << 20;
|
||||||
|
/// Janela de busca da continuação, em bytes, depois e antes do primeiro fragmento.
|
||||||
|
const WINDOW_AFTER: u64 = 512 << 20;
|
||||||
|
const WINDOW_BEFORE: u64 = 64 << 20;
|
||||||
|
const PROBE: usize = 8192;
|
||||||
|
pub const CLUSTER: u64 = 4096;
|
||||||
|
const MAX_PROBES: usize = 20_000;
|
||||||
|
|
||||||
|
/// Marca os clusters (de 4 KiB, alinhados ao início da origem) que parecem dados entropy-coded
|
||||||
|
/// de JPEG: sem marcadores no meio e com bytes bem distribuídos.
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct ClusterIndex {
|
||||||
|
bits: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClusterIndex {
|
||||||
|
pub fn new(size: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
bits: vec![0; (size / CLUSTER / 64 + 1) as usize],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set(&mut self, off: u64) {
|
||||||
|
let c = (off / CLUSTER) as usize;
|
||||||
|
if let Some(w) = self.bits.get_mut(c / 64) {
|
||||||
|
*w |= 1 << (c % 64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get(&self, off: u64) -> bool {
|
||||||
|
let c = (off / CLUSTER) as usize;
|
||||||
|
self.bits
|
||||||
|
.get(c / 64)
|
||||||
|
.map(|w| w >> (c % 64) & 1 == 1)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
/// Examina um trecho lido e marca os clusters candidatos.
|
||||||
|
pub fn scan_chunk(&mut self, abs: u64, buf: &[u8]) {
|
||||||
|
let first = abs.div_ceil(CLUSTER) * CLUSTER;
|
||||||
|
let mut o = first;
|
||||||
|
while o + CLUSTER <= abs + buf.len() as u64 {
|
||||||
|
let c = &buf[(o - abs) as usize..(o - abs + CLUSTER) as usize];
|
||||||
|
if jpeg_like(c) {
|
||||||
|
self.set(o);
|
||||||
|
}
|
||||||
|
o += CLUSTER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn iter_range(&self, from: u64, to: u64) -> impl Iterator<Item = u64> + '_ {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jpeg_like(c: &[u8]) -> bool {
|
||||||
|
let mut seen = [false; 256];
|
||||||
|
let mut distinct = 0;
|
||||||
|
for (i, &b) in c.iter().enumerate() {
|
||||||
|
if !seen[b as usize] {
|
||||||
|
seen[b as usize] = true;
|
||||||
|
distinct += 1;
|
||||||
|
}
|
||||||
|
if b == 0xFF {
|
||||||
|
match c.get(i + 1) {
|
||||||
|
Some(&n) if n == 0x00 || (0xD0..=0xD7).contains(&n) => {}
|
||||||
|
Some(0xD9) if i + 2 >= c.len() - 16 => {}
|
||||||
|
None => {}
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
distinct >= 190
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Pool<'a> {
|
||||||
|
pub index: &'a ClusterIndex,
|
||||||
|
/// Extents já atribuídos a outros arquivos.
|
||||||
|
pub used: &'a [Extent],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pool<'_> {
|
||||||
|
fn free_at(&self, off: u64, len: u64) -> bool {
|
||||||
|
!self
|
||||||
|
.used
|
||||||
|
.iter()
|
||||||
|
.any(|u| off < u.end() && off + len > u.offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Analysis {
|
||||||
|
/// Contíguo e consistente; `len` inclui o EOI.
|
||||||
|
Intact { len: u64 },
|
||||||
|
/// Remontado a partir de fragmentos.
|
||||||
|
Rebuilt { extents: Vec<Extent>, size: u64 },
|
||||||
|
/// Só o início é confiável.
|
||||||
|
Broken { good_len: u64 },
|
||||||
|
/// Formato que não sabemos validar (progressivo etc.).
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Procura FF D9 logo depois do último MCU (até 64 bytes de preenchimento).
|
||||||
|
fn eoi_after(data: &[u8], pos: usize) -> Option<usize> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
let read_len = (hit_len as usize).clamp(4096, MAX_FILE);
|
||||||
|
let Ok(buf) = read_vec(src, off, read_len) else {
|
||||||
|
return Analysis::Unsupported;
|
||||||
|
};
|
||||||
|
let Some(h) = jpegscan::parse_header(&buf) else {
|
||||||
|
return Analysis::Unsupported;
|
||||||
|
};
|
||||||
|
let d = jpegscan::decode(&h, &buf, State::start(&h), u32::MAX, 512);
|
||||||
|
let disc = jpegscan::first_discontinuity(&h, &d.dc);
|
||||||
|
if d.stop == Stop::Done && disc.is_none() {
|
||||||
|
if let Some(end) = eoi_after(&buf, d.state.pos) {
|
||||||
|
return Analysis::Intact { len: end as u64 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut stop_pos = match d.stop {
|
||||||
|
Stop::Marker(_, p) | Stop::Error(p) => p,
|
||||||
|
Stop::Truncated | Stop::Done => buf.len(),
|
||||||
|
};
|
||||||
|
if let Some(m) = disc {
|
||||||
|
let bad = d
|
||||||
|
.snaps
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|s| (s.mcu as usize) <= m)
|
||||||
|
.map(|s| s.pos)
|
||||||
|
.unwrap_or(h.scan_start);
|
||||||
|
stop_pos = stop_pos.min(bad);
|
||||||
|
}
|
||||||
|
let cl = CLUSTER as usize;
|
||||||
|
if off % CLUSTER != 0 || !allow_rebuild {
|
||||||
|
return Analysis::Broken {
|
||||||
|
good_len: (stop_pos / 512 * 512).max(512) as u64,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pieces: Vec<(u64, usize)> = vec![];
|
||||||
|
let mut joined = buf;
|
||||||
|
let mut cur_base = off; // início absoluto do fragmento atual
|
||||||
|
let mut frag_start = 0usize; // posição em `joined` onde o fragmento atual começa
|
||||||
|
let mut snaps = d.snaps;
|
||||||
|
let mut suspect = stop_pos;
|
||||||
|
|
||||||
|
for _ in 0..3 {
|
||||||
|
let top = suspect.saturating_sub(frag_start) / cl * cl;
|
||||||
|
let mut best: Option<(usize, u64, State, f64)> = None;
|
||||||
|
let mut probes = 0usize;
|
||||||
|
for back in 0..4usize {
|
||||||
|
let Some(bc_rel) = top.checked_sub(back * cl).filter(|&b| b > 0) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let bc = frag_start + bc_rel;
|
||||||
|
let Some(snap) = snaps
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|s| s.pos <= bc)
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| (frag_start == 0).then(|| State::start(&h)))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut plane = jpegscan::Plane::new(&h);
|
||||||
|
let _ =
|
||||||
|
jpegscan::decode_plane(&h, &joined[..bc], State::start(&h), u32::MAX, &mut plane);
|
||||||
|
let lo = off.saturating_sub(WINDOW_BEFORE);
|
||||||
|
let hi = cur_base + bc_rel as u64 + WINDOW_AFTER;
|
||||||
|
let pre = jpegscan::decode(&h, &joined[..bc], State::start(&h), u32::MAX, 0);
|
||||||
|
let bpm_prefix =
|
||||||
|
(pre.state.pos.saturating_sub(h.scan_start)) as f64 / pre.dc.len().max(1) as f64;
|
||||||
|
let mut ranked: Vec<(f64, u64)> = vec![];
|
||||||
|
for q in pool.index.iter_range(lo, hi) {
|
||||||
|
if q >= cur_base && q < cur_base + bc_rel as u64 + CLUSTER {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !pool.free_at(q, CLUSTER)
|
||||||
|
|| pieces
|
||||||
|
.iter()
|
||||||
|
.any(|&(po, pl)| q < po + pl as u64 && q + CLUSTER > po)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
probes += 1;
|
||||||
|
if probes > MAX_PROBES {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(sc) =
|
||||||
|
probe_candidate(src, &h, &joined[..bc], &plane, &snap, q, bpm_prefix)
|
||||||
|
{
|
||||||
|
ranked.push((sc, q));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ranked.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||||
|
ranked.truncate(120);
|
||||||
|
// Etapa 2: a continuação certa fecha a imagem exatamente no último MCU.
|
||||||
|
let mut longest: Option<(usize, u64, f64)> = None;
|
||||||
|
for (sc, q) in ranked {
|
||||||
|
let avail = (src.size() - q) as usize;
|
||||||
|
let more = read_vec(src, q, MAX_FILE.min(avail)).unwrap_or_default();
|
||||||
|
let mut j2 = joined[..bc].to_vec();
|
||||||
|
j2.extend_from_slice(&more);
|
||||||
|
let d2 = jpegscan::decode(&h, &j2, snap.clone(), u32::MAX, 0);
|
||||||
|
if d2.stop == Stop::Done && eoi_after(&j2, d2.state.pos).is_some() {
|
||||||
|
best = Some((bc, q, snap.clone(), 0.0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let reach = match d2.stop {
|
||||||
|
Stop::Marker(_, p) | Stop::Error(p) => p.saturating_sub(bc),
|
||||||
|
_ => j2.len() - bc,
|
||||||
|
};
|
||||||
|
if reach >= 3 * cl && longest.map_or(true, |l| sc < l.2) {
|
||||||
|
longest = Some((reach, q, sc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if best.is_none() {
|
||||||
|
if let Some((_, q, sc)) = longest {
|
||||||
|
if sc < 25.0 {
|
||||||
|
let cand = (bc, q, snap.clone(), sc);
|
||||||
|
if best.as_ref().map_or(true, |b| sc < b.3) {
|
||||||
|
best = Some(cand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if best.as_ref().is_some_and(|b| b.3 == 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some((bc, q, snap, _)) = best else { break };
|
||||||
|
pieces.push((cur_base, bc - frag_start));
|
||||||
|
let more = read_vec(src, q, MAX_FILE.min((src.size() - q) as usize)).unwrap_or_default();
|
||||||
|
joined.truncate(bc);
|
||||||
|
joined.extend_from_slice(&more);
|
||||||
|
let d2 = jpegscan::decode(&h, &joined, snap, u32::MAX, 512);
|
||||||
|
if d2.stop == Stop::Done {
|
||||||
|
if let Some(end) = eoi_after(&joined, d2.state.pos) {
|
||||||
|
pieces.push((q, end - bc));
|
||||||
|
return finish(pieces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Quebrou de novo: onde?
|
||||||
|
let mut sp = match d2.stop {
|
||||||
|
Stop::Marker(_, p) | Stop::Error(p) => p,
|
||||||
|
_ => joined.len(),
|
||||||
|
};
|
||||||
|
let all = jpegscan::decode(&h, &joined, State::start(&h), u32::MAX, 0);
|
||||||
|
if let Some(m) = jpegscan::first_discontinuity(&h, &all.dc) {
|
||||||
|
let bad = d2
|
||||||
|
.snaps
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|s| (s.mcu as usize) <= m)
|
||||||
|
.map(|s| s.pos)
|
||||||
|
.unwrap_or(bc);
|
||||||
|
if bad > bc {
|
||||||
|
sp = sp.min(bad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snaps = d2.snaps;
|
||||||
|
cur_base = q;
|
||||||
|
frag_start = bc;
|
||||||
|
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;
|
||||||
|
Analysis::Broken {
|
||||||
|
good_len: (stop_pos.min(good.max(512)) / 512 * 512).max(512) as u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(pieces: Vec<(u64, usize)>) -> Analysis {
|
||||||
|
let mut extents: Vec<Extent> = vec![];
|
||||||
|
for (o, l) in &pieces {
|
||||||
|
match extents.last_mut() {
|
||||||
|
Some(last) if last.end() == *o => last.len += *l as u64,
|
||||||
|
_ => extents.push(Extent::new(*o, *l as u64)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let size = pieces.iter().map(|p| p.1 as u64).sum();
|
||||||
|
if extents.len() == 1 {
|
||||||
|
return Analysis::Intact { len: size };
|
||||||
|
}
|
||||||
|
Analysis::Rebuilt { extents, size }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pontuação (menor é melhor) de `q` como continuação de `prefix`: diferença média de pixels
|
||||||
|
/// nas bordas entre os MCUs novos e seus vizinhos. Natural: poucos níveis de cinza; lixo: dezenas.
|
||||||
|
fn probe_candidate(
|
||||||
|
src: &dyn Source,
|
||||||
|
h: &Header,
|
||||||
|
prefix: &[u8],
|
||||||
|
plane: &jpegscan::Plane,
|
||||||
|
snap: &State,
|
||||||
|
q: u64,
|
||||||
|
bpm_prefix: f64,
|
||||||
|
) -> Option<f64> {
|
||||||
|
let more = read_vec(src, q, PROBE).ok()?;
|
||||||
|
if more.len() < 1024 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut joined = Vec::with_capacity(prefix.len() + more.len());
|
||||||
|
joined.extend_from_slice(prefix);
|
||||||
|
joined.extend_from_slice(&more);
|
||||||
|
let seam = plane.have;
|
||||||
|
const K: u32 = 36;
|
||||||
|
let mut news: std::collections::HashMap<u32, Vec<u8>> = std::collections::HashMap::new();
|
||||||
|
let mut hook = |mcu: u32, blocks: &[[i32; 64]]| {
|
||||||
|
if mcu >= seam {
|
||||||
|
news.insert(mcu, jpegscan::mcu_luma(h, blocks));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let want = seam.saturating_sub(snap.mcu) + K + 1;
|
||||||
|
let d = jpegscan::decode_hook(h, &joined, snap.clone(), want, 0, Some(&mut hook));
|
||||||
|
match d.stop {
|
||||||
|
Stop::Error(_) => return None,
|
||||||
|
Stop::Marker(m, _) if m != 0xD9 => return None,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
let (mw, mh) = (plane.mw, plane.mh);
|
||||||
|
let mx = h.mcus_x;
|
||||||
|
let get = |idx: u32| -> Option<Vec<u8>> {
|
||||||
|
if idx < seam {
|
||||||
|
let (x, y) = ((idx % mx) as usize, (idx / mx) as usize);
|
||||||
|
let mut v = vec![0u8; mw * mh];
|
||||||
|
for r in 0..mh {
|
||||||
|
let o = (y * mh + r) * plane.w + x * mw;
|
||||||
|
v[r * mw..(r + 1) * mw].copy_from_slice(&plane.px[o..o + mw]);
|
||||||
|
}
|
||||||
|
Some(v)
|
||||||
|
} else {
|
||||||
|
news.get(&idx).cloned()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (mut sum, mut n) = (0f64, 0u32);
|
||||||
|
for idx in seam + 1..seam + 1 + K {
|
||||||
|
let Some(cur) = news.get(&idx) else { break };
|
||||||
|
if idx % mx > 0 {
|
||||||
|
if let Some(l) = get(idx - 1) {
|
||||||
|
for y in 0..mh {
|
||||||
|
sum += (cur[y * mw] as i32 - l[y * mw + mw - 1] as i32).abs() as f64;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idx >= mx {
|
||||||
|
if let Some(t) = get(idx - mx) {
|
||||||
|
for x in 0..mw {
|
||||||
|
sum += (cur[x] as i32 - t[(mh - 1) * mw + x] as i32).abs() as f64;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n < 40 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Bytes por MCU depois da emenda, comparados com os do trecho anterior.
|
||||||
|
let after = d.dc.len() as f64 - (seam.saturating_sub(snap.mcu)) as f64;
|
||||||
|
let used = d.state.pos.saturating_sub(prefix.len()) as f64;
|
||||||
|
let ratio = if after > 4.0 && used > 0.0 {
|
||||||
|
(used / after) / bpm_prefix.max(0.5)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let sc = sum / n as f64 + 20.0 * ratio.ln().abs();
|
||||||
|
Some(sc)
|
||||||
|
}
|
||||||
@@ -0,0 +1,651 @@
|
|||||||
|
//! APFS (volumes sem cifragem): lê o contêiner, o mapa de objetos e a árvore de arquivos de cada
|
||||||
|
//! volume; arquivos apagados vêm de duas fontes: as árvores dos instantâneos (snapshots) e os nós
|
||||||
|
//! antigos da árvore (copy-on-write) que ainda estão no disco. Volumes cifrados (FileVault) são
|
||||||
|
//! detectados e deixados para a varredura por assinatura.
|
||||||
|
|
||||||
|
use super::{make_found, Ctx, NewFound, VolumeScan};
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use crate::types::{Confidence, DataLoc, Extent, Origin};
|
||||||
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
|
fn le16(b: &[u8], o: usize) -> u16 {
|
||||||
|
b.get(o..o + 2)
|
||||||
|
.map(|s| u16::from_le_bytes([s[0], s[1]]))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OBJ_TYPE_MASK: u32 = 0xFFFF;
|
||||||
|
const T_BTREE_ROOT: u32 = 2;
|
||||||
|
const T_BTREE_NODE: u32 = 3;
|
||||||
|
const SUBTYPE_FSTREE: u32 = 0x0E;
|
||||||
|
|
||||||
|
// tipos de registro da árvore de arquivos
|
||||||
|
const R_SNAP_METADATA: u8 = 1;
|
||||||
|
const R_INODE: u8 = 3;
|
||||||
|
const R_FILE_EXTENT: u8 = 8;
|
||||||
|
const R_DIR_REC: u8 = 9;
|
||||||
|
|
||||||
|
/// Soma de verificação Fletcher-64 do APFS (cobre o bloco menos os 8 bytes iniciais).
|
||||||
|
pub fn fletcher64(data: &[u8]) -> u64 {
|
||||||
|
let (mut s1, mut s2) = (0u64, 0u64);
|
||||||
|
for w in data.chunks_exact(4) {
|
||||||
|
s1 = (s1 + u32::from_le_bytes(w.try_into().unwrap()) as u64) % 0xFFFF_FFFF;
|
||||||
|
s2 = (s2 + s1) % 0xFFFF_FFFF;
|
||||||
|
}
|
||||||
|
let c1 = 0xFFFF_FFFF - ((s1 + s2) % 0xFFFF_FFFF);
|
||||||
|
let c2 = 0xFFFF_FFFF - ((s1 + c1) % 0xFFFF_FFFF);
|
||||||
|
(c2 << 32) | c1
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Apfs<'a> {
|
||||||
|
src: &'a dyn Source,
|
||||||
|
base: u64,
|
||||||
|
bs: u64,
|
||||||
|
blocks: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BNode {
|
||||||
|
flags: u16,
|
||||||
|
level: u16,
|
||||||
|
/// (chave, valor) por entrada
|
||||||
|
entries: Vec<(Vec<u8>, Vec<u8>)>,
|
||||||
|
oid: u64,
|
||||||
|
xid: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Apfs<'a> {
|
||||||
|
fn block(&self, paddr: u64) -> Option<Vec<u8>> {
|
||||||
|
if paddr >= self.blocks {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let b = read_vec(self.src, self.base + paddr * self.bs, self.bs as usize).ok()?;
|
||||||
|
(b.len() as u64 == self.bs).then_some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checked_block(&self, paddr: u64) -> Option<Vec<u8>> {
|
||||||
|
let b = self.block(paddr)?;
|
||||||
|
(fletcher64(&b[8..]) == le64(&b, 0)).then_some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_node(&self, b: &[u8], fixed_k: usize, fixed_v: usize) -> Option<BNode> {
|
||||||
|
let flags = le16(b, 32);
|
||||||
|
let level = le16(b, 34);
|
||||||
|
let n = le32(b, 36) as usize;
|
||||||
|
let toc_off = le16(b, 40) as usize;
|
||||||
|
let toc_len = le16(b, 42) as usize;
|
||||||
|
let is_fixed = flags & 4 != 0;
|
||||||
|
let is_root = flags & 1 != 0;
|
||||||
|
let key_base = 56 + toc_off + toc_len;
|
||||||
|
let val_end = b.len() - if is_root { 40 } else { 0 };
|
||||||
|
let mut entries = Vec::with_capacity(n);
|
||||||
|
for i in 0..n {
|
||||||
|
let (koff, klen, voff, vlen);
|
||||||
|
if is_fixed {
|
||||||
|
let o = 56 + toc_off + i * 4;
|
||||||
|
koff = le16(b, o) as usize;
|
||||||
|
voff = le16(b, o + 2) as usize;
|
||||||
|
klen = fixed_k;
|
||||||
|
vlen = if level == 0 { fixed_v } else { 8 };
|
||||||
|
} else {
|
||||||
|
let o = 56 + toc_off + i * 8;
|
||||||
|
koff = le16(b, o) as usize;
|
||||||
|
klen = le16(b, o + 2) as usize;
|
||||||
|
voff = le16(b, o + 4) as usize;
|
||||||
|
vlen = le16(b, o + 6) as usize;
|
||||||
|
}
|
||||||
|
let ks = key_base + koff;
|
||||||
|
let k = b.get(ks..ks + klen)?.to_vec();
|
||||||
|
let v = if voff == 0xFFFF {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
b.get(val_end.checked_sub(voff)?..)?.get(..vlen)?.to_vec()
|
||||||
|
};
|
||||||
|
entries.push((k, v));
|
||||||
|
}
|
||||||
|
Some(BNode {
|
||||||
|
flags,
|
||||||
|
level,
|
||||||
|
entries,
|
||||||
|
oid: le64(b, 8),
|
||||||
|
xid: le64(b, 16),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Todas as entradas de uma árvore física (mapa de objetos): filhos são endereços físicos.
|
||||||
|
fn walk_physical(
|
||||||
|
&self,
|
||||||
|
root: u64,
|
||||||
|
fixed_k: usize,
|
||||||
|
fixed_v: usize,
|
||||||
|
f: &mut dyn FnMut(&[u8], &[u8]),
|
||||||
|
) {
|
||||||
|
let mut stack = vec![(root, 0u32)];
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
while let Some((p, depth)) = stack.pop() {
|
||||||
|
if depth > 8 || !seen.insert(p) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(b) = self.checked_block(p) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(n) = self.parse_node(&b, fixed_k, fixed_v) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if n.level == 0 {
|
||||||
|
for (k, v) in &n.entries {
|
||||||
|
f(k, v);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (_, v) in n.entries.iter().rev() {
|
||||||
|
stack.push((le64(v, 0), depth + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mapa de objetos: oid -> [(xid, paddr)] ordenado por xid.
|
||||||
|
fn omap(&self, omap_paddr: u64) -> HashMap<u64, Vec<(u64, u64)>> {
|
||||||
|
let mut m: HashMap<u64, Vec<(u64, u64)>> = HashMap::new();
|
||||||
|
let Some(ob) = self.checked_block(omap_paddr) else {
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
let tree = le64(&ob, 48);
|
||||||
|
self.walk_physical(tree, 16, 16, &mut |k, v| {
|
||||||
|
m.entry(le64(k, 0))
|
||||||
|
.or_default()
|
||||||
|
.push((le64(k, 8), le64(v, 8)));
|
||||||
|
});
|
||||||
|
for v in m.values_mut() {
|
||||||
|
v.sort();
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- registros
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct Inode {
|
||||||
|
parent: u64,
|
||||||
|
private_id: u64,
|
||||||
|
mtime: i64,
|
||||||
|
ctime: i64,
|
||||||
|
atime: i64,
|
||||||
|
mode: u16,
|
||||||
|
size: u64,
|
||||||
|
name: Option<String>,
|
||||||
|
has: bool,
|
||||||
|
compressed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct FileRec {
|
||||||
|
inode: Inode,
|
||||||
|
names: BTreeMap<u64, String>, // pai -> nome
|
||||||
|
}
|
||||||
|
|
||||||
|
type Files = HashMap<u64, FileRec>;
|
||||||
|
type Extents = HashMap<u64, BTreeMap<u64, (u64, u64, u64)>>; // private_id -> logical -> (len, paddr, crypto)
|
||||||
|
|
||||||
|
fn ns(t: u64) -> i64 {
|
||||||
|
(t / 1_000_000_000) as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
|
||||||
|
let hdr = le64(k, 0);
|
||||||
|
let (id, ty) = (hdr & 0x0FFF_FFFF_FFFF_FFFF, (hdr >> 60) as u8);
|
||||||
|
match ty {
|
||||||
|
R_INODE if v.len() >= 92 => {
|
||||||
|
let e = files.entry(id).or_default();
|
||||||
|
let mut ino = Inode {
|
||||||
|
parent: le64(v, 0),
|
||||||
|
private_id: le64(v, 8),
|
||||||
|
ctime: ns(le64(v, 16)), // criação
|
||||||
|
mtime: ns(le64(v, 24)),
|
||||||
|
atime: ns(le64(v, 40)),
|
||||||
|
mode: le16(v, 80),
|
||||||
|
size: le64(v, 84),
|
||||||
|
has: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let flags = le64(v, 48);
|
||||||
|
ino.compressed = le32(v, 68) & 0x20 != 0;
|
||||||
|
let _ = flags;
|
||||||
|
// campos estendidos
|
||||||
|
let xf = &v[92..];
|
||||||
|
if xf.len() >= 4 {
|
||||||
|
let n = le16(xf, 0) as usize;
|
||||||
|
let mut p = 4 + n * 4;
|
||||||
|
for i in 0..n {
|
||||||
|
let (xt, xs) = (xf[4 + i * 4], le16(xf, 6 + i * 4) as usize);
|
||||||
|
let data = xf.get(p..p + xs).unwrap_or(&[]);
|
||||||
|
match xt {
|
||||||
|
4 => {
|
||||||
|
ino.name = Some(
|
||||||
|
String::from_utf8_lossy(
|
||||||
|
data.split(|&c| c == 0).next().unwrap_or(&[]),
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
8 if data.len() >= 8 => ino.size = le64(data, 0),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
p += xs.div_ceil(8) * 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.inode = ino;
|
||||||
|
}
|
||||||
|
R_DIR_REC if k.len() > 12 && v.len() >= 18 => {
|
||||||
|
let nl = (le32(k, 8) & 0x3FF) as usize;
|
||||||
|
if let Some(name) = k.get(12..12 + nl) {
|
||||||
|
let name = String::from_utf8_lossy(name.split(|&c| c == 0).next().unwrap_or(&[]))
|
||||||
|
.to_string();
|
||||||
|
files.entry(le64(v, 0)).or_default().names.insert(id, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R_FILE_EXTENT if k.len() >= 16 && v.len() >= 24 => {
|
||||||
|
let len = le64(v, 0) & 0x00FF_FFFF_FFFF_FFFF;
|
||||||
|
extents
|
||||||
|
.entry(id)
|
||||||
|
.or_default()
|
||||||
|
.insert(le64(k, 8), (len, le64(v, 8), le64(v, 16)));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apfs<'_> {
|
||||||
|
/// 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),
|
||||||
|
) {
|
||||||
|
let Some(rp) = lookup(omap, root_oid, xid) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut stack = vec![(rp, 0u32)];
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
while let Some((p, depth)) = stack.pop() {
|
||||||
|
if depth > 10 || !seen.insert(p) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(b) = self.checked_block(p) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(n) = self.parse_node(&b, 16, 16) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if n.level == 0 {
|
||||||
|
for (k, v) in &n.entries {
|
||||||
|
f(k, v, p);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (_, v) in n.entries.iter().rev() {
|
||||||
|
if let Some(cp) = lookup(omap, le64(v, 0), xid) {
|
||||||
|
stack.push((cp, depth + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_of(files: &Files, mut id: u64) -> String {
|
||||||
|
let mut parts = vec![];
|
||||||
|
for _ in 0..64 {
|
||||||
|
if id == 2 || id == 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match files
|
||||||
|
.get(&id)
|
||||||
|
.and_then(|f| f.names.iter().next().map(|(p, n)| (*p, n.clone())))
|
||||||
|
{
|
||||||
|
Some((p, n)) => {
|
||||||
|
parts.push(n);
|
||||||
|
id = p;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.reverse();
|
||||||
|
parts.join("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Volume {
|
||||||
|
name: String,
|
||||||
|
omap_paddr: u64,
|
||||||
|
root_oid: u64,
|
||||||
|
snap_tree_oid: u64,
|
||||||
|
encrypted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_volume(b: &[u8]) -> Option<Volume> {
|
||||||
|
if &b[32..36] != b"APSB" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = String::from_utf8_lossy(&b[704..960])
|
||||||
|
.split('\0')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
Some(Volume {
|
||||||
|
name,
|
||||||
|
omap_paddr: le64(b, 128),
|
||||||
|
root_oid: le64(b, 136),
|
||||||
|
snap_tree_oid: le64(b, 152),
|
||||||
|
encrypted: le64(b, 264) & 1 == 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_container<'a>(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec<u8>)> {
|
||||||
|
let b0 = read_vec(src, base, 4096).ok()?;
|
||||||
|
if b0.len() < 4096 || &b0[32..36] != b"NXSB" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bs = le32(&b0, 36) as u64;
|
||||||
|
let blocks = le64(&b0, 40);
|
||||||
|
if !(4096..=65536).contains(&bs) || !bs.is_power_of_two() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let a = Apfs {
|
||||||
|
src,
|
||||||
|
base,
|
||||||
|
bs,
|
||||||
|
blocks,
|
||||||
|
};
|
||||||
|
// O superbloco mais recente está na área de descritores de checkpoint.
|
||||||
|
let desc_blocks = le32(&b0, 104) as u64;
|
||||||
|
let desc_base = le64(&b0, 112);
|
||||||
|
let mut best = b0.clone();
|
||||||
|
for i in 0..desc_blocks {
|
||||||
|
if let Some(b) = a.checked_block(desc_base as u64 + i) {
|
||||||
|
if &b[32..36] == b"NXSB" && le64(&b, 16) > le64(&best, 16) {
|
||||||
|
best = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((a, best))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
||||||
|
let _ = part_len;
|
||||||
|
let mut res = VolumeScan::default();
|
||||||
|
let Some((a, nx)) = open_container(ctx.src, base) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
let Some(cont_omap_block) = a.checked_block(le64(&nx, 160)) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
let _ = cont_omap_block;
|
||||||
|
let container_omap = a.omap(le64(&nx, 160));
|
||||||
|
let max_fs = le32(&nx, 180).min(100) as usize;
|
||||||
|
let mut vols: Vec<Volume> = vec![];
|
||||||
|
for i in 0..max_fs {
|
||||||
|
let oid = le64(&nx, 184 + i * 8);
|
||||||
|
if oid == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(p) = lookup(&container_omap, oid, u64::MAX) {
|
||||||
|
if let Some(b) = a.checked_block(p) {
|
||||||
|
if let Some(v) = parse_volume(&b) {
|
||||||
|
vols.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let nvols = vols.len();
|
||||||
|
for v in &vols {
|
||||||
|
if v.encrypted {
|
||||||
|
res.warnings.push(format!("APFS: o volume \"{}\" está cifrado (FileVault); os arquivos só são legíveis com a chave, então use a varredura por assinatura apenas se o volume já estiver aberto.", v.name));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
scan_volume(ctx, &a, v, nvols == 1, &mut res);
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: &mut VolumeScan) {
|
||||||
|
let omap = a.omap(v.omap_paddr);
|
||||||
|
let current_xid = u64::MAX;
|
||||||
|
// Árvore atual.
|
||||||
|
let mut live: Files = HashMap::new();
|
||||||
|
let mut live_ext: Extents = HashMap::new();
|
||||||
|
let mut live_nodes: HashSet<u64> = HashSet::new();
|
||||||
|
a.walk_fstree(v.root_oid, &omap, current_xid, &mut |k, val, node| {
|
||||||
|
live_nodes.insert(node);
|
||||||
|
add_record(&mut live, &mut live_ext, k, val);
|
||||||
|
});
|
||||||
|
// Instantâneos.
|
||||||
|
let mut stale: Files = HashMap::new();
|
||||||
|
let mut stale_ext: Extents = HashMap::new();
|
||||||
|
if v.snap_tree_oid != 0 {
|
||||||
|
if let Some(sp) = lookup(&omap, v.snap_tree_oid, current_xid).or(Some(v.snap_tree_oid)) {
|
||||||
|
let mut snaps: Vec<(u64, u64)> = vec![]; // (xid, sblock paddr)
|
||||||
|
let mut stack = vec![sp];
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
while let Some(p) = stack.pop() {
|
||||||
|
if !seen.insert(p) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(b) = a.checked_block(p) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(n) = a.parse_node(&b, 0, 0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if n.level == 0 {
|
||||||
|
for (k, val) in &n.entries {
|
||||||
|
let hdr = le64(k, 0);
|
||||||
|
if (hdr >> 60) as u8 == R_SNAP_METADATA && val.len() >= 40 {
|
||||||
|
snaps.push((hdr & 0x0FFF_FFFF_FFFF_FFFF, le64(val, 8)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (_, val) in &n.entries {
|
||||||
|
// árvore de metadados de snapshot: filhos físicos ou virtuais
|
||||||
|
let c = le64(val, 0);
|
||||||
|
stack.push(lookup(&omap, c, current_xid).unwrap_or(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (xid, sblock) in snaps {
|
||||||
|
let Some(sb) = a.checked_block(sblock) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(sv) = parse_volume(&sb) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
a.walk_fstree(sv.root_oid, &omap, xid, &mut |k, val, _| {
|
||||||
|
add_record(&mut stale, &mut stale_ext, k, val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nós antigos da árvore de arquivos.
|
||||||
|
if allow_stale_scan {
|
||||||
|
let total = a.blocks;
|
||||||
|
for p in 0..total {
|
||||||
|
if p % 4096 == 0 {
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(ctx.progress)(p, total);
|
||||||
|
}
|
||||||
|
let Some(b) = a.block(p) else { continue };
|
||||||
|
let ty = le32(&b, 24) & OBJ_TYPE_MASK;
|
||||||
|
if (ty != T_BTREE_NODE && ty != T_BTREE_ROOT)
|
||||||
|
|| le32(&b, 28) != SUBTYPE_FSTREE
|
||||||
|
|| le16(&b, 34) != 0
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if live_nodes.contains(&p) || fletcher64(&b[8..]) != le64(&b, 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(n) = a.parse_node(&b, 16, 16) {
|
||||||
|
let _ = (n.oid, n.xid, n.flags);
|
||||||
|
for (k, val) in &n.entries {
|
||||||
|
add_record(&mut stale, &mut stale_ext, k, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Arquivos que só existem nas versões antigas.
|
||||||
|
let mut ids: Vec<&u64> = stale.keys().collect();
|
||||||
|
ids.sort();
|
||||||
|
let mut seen_first: HashSet<u64> = HashSet::new();
|
||||||
|
for id in ids {
|
||||||
|
if live.get(id).is_some_and(|f| f.inode.has) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let rec = &stale[id];
|
||||||
|
let ino = &rec.inode;
|
||||||
|
if !ino.has || ino.mode & 0o170000 != 0o100000 || ino.size == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(ex) = stale_ext.get(&ino.private_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if ex.values().any(|e| e.2 != 0) {
|
||||||
|
continue; // extensão cifrada
|
||||||
|
}
|
||||||
|
// nome e pasta: da entrada de diretório, senão do próprio inode
|
||||||
|
let (name, parent) = match rec.names.iter().next() {
|
||||||
|
Some((p, n)) => (n.clone(), *p),
|
||||||
|
None => match &ino.name {
|
||||||
|
Some(n) => (n.clone(), 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 dir = Some(format!("/{}", path_of(&merged, parent)).replace("//", "/"));
|
||||||
|
let mut extents: Vec<Extent> = vec![];
|
||||||
|
let mut pos = 0u64;
|
||||||
|
let mut ok = true;
|
||||||
|
for (&lo, &(len, paddr, _)) in ex {
|
||||||
|
if lo > pos {
|
||||||
|
extents.push(Extent {
|
||||||
|
offset: 0,
|
||||||
|
len: lo - pos,
|
||||||
|
sparse: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if paddr == 0 {
|
||||||
|
extents.push(Extent {
|
||||||
|
offset: 0,
|
||||||
|
len,
|
||||||
|
sparse: true,
|
||||||
|
});
|
||||||
|
} else if paddr < a.blocks {
|
||||||
|
extents.push(Extent::new(a.base + paddr * a.bs, len));
|
||||||
|
} else {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
pos = lo + len;
|
||||||
|
}
|
||||||
|
if !ok || pos < ino.size.min(1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let extents = super::fat::trim_extents(extents, ino.size);
|
||||||
|
let Some(first) = extents.iter().find(|e| !e.sparse) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !seen_first.insert(first.offset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let note = if ino.compressed {
|
||||||
|
"arquivo comprimido pelo macOS (decmpfs): o conteúdo pode estar incompleto"
|
||||||
|
} else {
|
||||||
|
"versão antiga da árvore ou instantâneo do APFS"
|
||||||
|
};
|
||||||
|
let ts = |t: i64| (t > 0).then_some(t);
|
||||||
|
let nf = NewFound {
|
||||||
|
name,
|
||||||
|
dir,
|
||||||
|
size: ino.size,
|
||||||
|
origin: Origin::Apfs,
|
||||||
|
data: DataLoc::Extents { extents },
|
||||||
|
modified: ts(ino.mtime),
|
||||||
|
created: ts(ino.ctime),
|
||||||
|
accessed: ts(ino.atime),
|
||||||
|
base: if ino.compressed {
|
||||||
|
Confidence::Low
|
||||||
|
} else {
|
||||||
|
Confidence::Medium
|
||||||
|
},
|
||||||
|
note: Some(note),
|
||||||
|
};
|
||||||
|
let f = make_found(ctx, nf);
|
||||||
|
(ctx.emit)(f);
|
||||||
|
}
|
||||||
|
let _ = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arquivos e pastas atuais do primeiro volume sem cifragem (caminho, tamanho, é pasta).
|
||||||
|
pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
|
||||||
|
let Some((a, nx)) = open_container(src, base) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let cont = a.omap(le64(&nx, 160));
|
||||||
|
let mut out = vec![];
|
||||||
|
for i in 0..le32(&nx, 180).min(100) as usize {
|
||||||
|
let oid = le64(&nx, 184 + i * 8);
|
||||||
|
let Some(p) = lookup(&cont, oid, u64::MAX) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(vb) = a.checked_block(p) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(v) = parse_volume(&vb) else { continue };
|
||||||
|
if v.encrypted {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let omap = a.omap(v.omap_paddr);
|
||||||
|
let mut files: Files = HashMap::new();
|
||||||
|
let mut ext: Extents = HashMap::new();
|
||||||
|
a.walk_fstree(v.root_oid, &omap, u64::MAX, &mut |k, val, _| {
|
||||||
|
add_record(&mut files, &mut ext, k, val)
|
||||||
|
});
|
||||||
|
for (&id, f) in &files {
|
||||||
|
if !f.inode.has || id <= 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push((
|
||||||
|
path_of(&files, id),
|
||||||
|
f.inode.size,
|
||||||
|
f.inode.mode & 0o170000 == 0o040000,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort();
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -0,0 +1,944 @@
|
|||||||
|
//! Btrfs (um dispositivo; perfis single, DUP e RAID 1): árvores atuais para nomes e mapa de
|
||||||
|
//! espaço livre, e arquivos apagados achados em nós antigos das árvores. Como o Btrfs nunca
|
||||||
|
//! sobrescreve metadados no lugar (copy-on-write), as versões anteriores dos nós continuam no
|
||||||
|
//! disco até o espaço ser reaproveitado: varremos os blocos de metadata em busca delas.
|
||||||
|
//! Extensões comprimidas (zlib, zstd, lzo) são descompactadas na recuperação.
|
||||||
|
|
||||||
|
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use crate::types::{Confidence, DataLoc, Extent, Origin, Packed, PackedCodec, Part};
|
||||||
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
|
const MAGIC: &[u8; 8] = b"_BHRfS_M";
|
||||||
|
const SB_OFF: u64 = 0x10000;
|
||||||
|
|
||||||
|
// chaves
|
||||||
|
const INODE_ITEM: u8 = 1;
|
||||||
|
const INODE_REF: u8 = 12;
|
||||||
|
const EXTENT_DATA: u8 = 108;
|
||||||
|
const ROOT_ITEM: u8 = 132;
|
||||||
|
const EXTENT_ITEM: u8 = 168;
|
||||||
|
const METADATA_ITEM: u8 = 169;
|
||||||
|
const CHUNK_ITEM: u8 = 228;
|
||||||
|
|
||||||
|
fn le16(b: &[u8], o: usize) -> u16 {
|
||||||
|
b.get(o..o + 2)
|
||||||
|
.map(|s| u16::from_le_bytes([s[0], s[1]]))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn crc32c(data: &[u8]) -> u32 {
|
||||||
|
static TABLE: std::sync::OnceLock<[u32; 256]> = std::sync::OnceLock::new();
|
||||||
|
let t = TABLE.get_or_init(|| {
|
||||||
|
let mut t = [0u32; 256];
|
||||||
|
for i in 0..256u32 {
|
||||||
|
let mut c = i;
|
||||||
|
for _ in 0..8 {
|
||||||
|
c = if c & 1 != 0 {
|
||||||
|
(c >> 1) ^ 0x82F6_3B78
|
||||||
|
} else {
|
||||||
|
c >> 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
t[i as usize] = c;
|
||||||
|
}
|
||||||
|
t
|
||||||
|
});
|
||||||
|
let mut c = !0u32;
|
||||||
|
for &b in data {
|
||||||
|
c = t[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
|
||||||
|
}
|
||||||
|
!c
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct Key {
|
||||||
|
pub objectid: u64,
|
||||||
|
pub ty: u8,
|
||||||
|
pub offset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Chunk {
|
||||||
|
logical: u64,
|
||||||
|
len: u64,
|
||||||
|
ty: u64,
|
||||||
|
/// (offset físico do primeiro stripe) por cópia
|
||||||
|
phys: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum NodeKind {
|
||||||
|
Leaf(Vec<(Key, Vec<u8>)>),
|
||||||
|
Internal(Vec<(Key, u64)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Node {
|
||||||
|
bytenr: u64,
|
||||||
|
generation: u64,
|
||||||
|
owner: u64,
|
||||||
|
kind: NodeKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fs<'a> {
|
||||||
|
src: &'a dyn Source,
|
||||||
|
base: u64,
|
||||||
|
nodesize: u64,
|
||||||
|
sectorsize: u64,
|
||||||
|
fsid: [u8; 16],
|
||||||
|
chunks: Vec<Chunk>,
|
||||||
|
root: u64,
|
||||||
|
chunk_root: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Fs<'a> {
|
||||||
|
fn open(src: &'a dyn Source, base: u64) -> Option<Fs<'a>> {
|
||||||
|
let sb = read_vec(src, base + SB_OFF, 4096).ok()?;
|
||||||
|
if sb.len() < 4096 || &sb[64..72] != MAGIC {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let csum_type = le16(&sb, 196);
|
||||||
|
if csum_type == 0 && crc32c(&sb[32..4096]) != le32(&sb, 0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let nodesize = le32(&sb, 148) as u64;
|
||||||
|
let sectorsize = le32(&sb, 144) as u64;
|
||||||
|
if !nodesize.is_power_of_two()
|
||||||
|
|| !(4096..=65536).contains(&nodesize)
|
||||||
|
|| !sectorsize.is_power_of_two()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut fsid = [0u8; 16];
|
||||||
|
fsid.copy_from_slice(&sb[32..48]);
|
||||||
|
let mut fs = Fs {
|
||||||
|
src,
|
||||||
|
base,
|
||||||
|
nodesize,
|
||||||
|
sectorsize,
|
||||||
|
fsid,
|
||||||
|
chunks: vec![],
|
||||||
|
root: le64(&sb, 80),
|
||||||
|
chunk_root: le64(&sb, 88),
|
||||||
|
};
|
||||||
|
// sys_chunk_array
|
||||||
|
let n = le32(&sb, 160) as usize;
|
||||||
|
let arr = sb.get(0x32B..0x32B + n.min(2048))?;
|
||||||
|
let mut p = 0;
|
||||||
|
while p + 17 + 48 <= arr.len() {
|
||||||
|
let k = key_at(arr, p);
|
||||||
|
if k.ty != CHUNK_ITEM {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let (ch, used) = parse_chunk(&arr[p + 17..], k.offset)?;
|
||||||
|
fs.chunks.push(ch);
|
||||||
|
p += 17 + used;
|
||||||
|
}
|
||||||
|
// chunk tree completa
|
||||||
|
let mut more = vec![];
|
||||||
|
let cr = fs.chunk_root;
|
||||||
|
fs.walk(cr, &mut |k, d| {
|
||||||
|
if k.ty == CHUNK_ITEM {
|
||||||
|
if let Some((ch, _)) = parse_chunk(d, k.offset) {
|
||||||
|
more.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for c in more {
|
||||||
|
if !fs.chunks.iter().any(|x| x.logical == c.logical) {
|
||||||
|
fs.chunks.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.chunks.sort_by_key(|c| c.logical);
|
||||||
|
Some(fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn phys(&self, logical: u64) -> Option<u64> {
|
||||||
|
let i = self
|
||||||
|
.chunks
|
||||||
|
.partition_point(|c| c.logical + c.len <= logical);
|
||||||
|
let c = self.chunks.get(i)?;
|
||||||
|
(c.logical <= logical)
|
||||||
|
.then(|| {
|
||||||
|
c.phys
|
||||||
|
.first()
|
||||||
|
.map(|p| self.base + p + (logical - c.logical))
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_node(&self, logical: u64) -> Option<Node> {
|
||||||
|
let p = self.phys(logical)?;
|
||||||
|
let raw = read_vec(self.src, p, self.nodesize as usize).ok()?;
|
||||||
|
self.parse_node(&raw, Some(logical))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_node(&self, raw: &[u8], expect: Option<u64>) -> Option<Node> {
|
||||||
|
if raw.len() as u64 != self.nodesize || raw[32..48] != self.fsid {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bytenr = le64(raw, 48);
|
||||||
|
if expect.is_some_and(|e| e != bytenr) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if crc32c(&raw[32..]) != le32(raw, 0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let nritems = le32(raw, 96) as usize;
|
||||||
|
let level = raw[100];
|
||||||
|
if nritems > (self.nodesize as usize) / 25 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let hdr = 101usize;
|
||||||
|
let kind = if level == 0 {
|
||||||
|
let mut items = Vec::with_capacity(nritems);
|
||||||
|
for i in 0..nritems {
|
||||||
|
let o = hdr + i * 25;
|
||||||
|
let k = key_at(raw, o);
|
||||||
|
let (off, size) = (le32(raw, o + 17) as usize, le32(raw, o + 21) as usize);
|
||||||
|
let s = hdr + off;
|
||||||
|
if s + size > raw.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
items.push((k, raw[s..s + size].to_vec()));
|
||||||
|
}
|
||||||
|
NodeKind::Leaf(items)
|
||||||
|
} else {
|
||||||
|
let mut ptrs = Vec::with_capacity(nritems);
|
||||||
|
for i in 0..nritems {
|
||||||
|
let o = hdr + i * 33;
|
||||||
|
if o + 33 > raw.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
ptrs.push((key_at(raw, o), le64(raw, o + 17)));
|
||||||
|
}
|
||||||
|
NodeKind::Internal(ptrs)
|
||||||
|
};
|
||||||
|
Some(Node {
|
||||||
|
bytenr,
|
||||||
|
generation: le64(raw, 80),
|
||||||
|
owner: le64(raw, 88),
|
||||||
|
kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Percorre uma árvore em ordem, chamando `f` para cada item de folha.
|
||||||
|
fn walk(&self, root: u64, f: &mut dyn FnMut(&Key, &[u8])) {
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
self.walk_inner(root, f, &mut seen, &mut |_| {}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_nodes(&self, root: u64, f: &mut dyn FnMut(&Key, &[u8]), nodes: &mut dyn FnMut(&Node)) {
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
if depth > 10 || !seen.insert(at) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(n) = self.read_node(at) else { return };
|
||||||
|
nodes(&n);
|
||||||
|
match &n.kind {
|
||||||
|
NodeKind::Leaf(items) => {
|
||||||
|
for (k, d) in items {
|
||||||
|
f(k, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeKind::Internal(ptrs) => {
|
||||||
|
for (_, child) in ptrs {
|
||||||
|
self.walk_inner(*child, f, seen, nodes, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_chunk(d: &[u8], logical: u64) -> Option<(Chunk, usize)> {
|
||||||
|
if d.len() < 48 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let len = le64(d, 0);
|
||||||
|
let ty = le64(d, 24);
|
||||||
|
let nstripes = le16(d, 44) as usize;
|
||||||
|
if nstripes == 0 || nstripes > 32 || d.len() < 48 + nstripes * 32 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let phys = (0..nstripes).map(|i| le64(d, 48 + i * 32 + 8)).collect();
|
||||||
|
Some((
|
||||||
|
Chunk {
|
||||||
|
logical,
|
||||||
|
len,
|
||||||
|
ty,
|
||||||
|
phys,
|
||||||
|
},
|
||||||
|
48 + nstripes * 32,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- inodes
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct InodeData {
|
||||||
|
gen: u64,
|
||||||
|
size: u64,
|
||||||
|
mode: u32,
|
||||||
|
mtime: i64,
|
||||||
|
ctime: i64,
|
||||||
|
atime: i64,
|
||||||
|
has_inode: bool,
|
||||||
|
refs: HashMap<u64, String>,
|
||||||
|
/// offset no arquivo -> (geração do nó, item)
|
||||||
|
extents: BTreeMap<u64, (u64, Vec<u8>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Files = HashMap<(u64, u64), InodeData>;
|
||||||
|
|
||||||
|
fn add_item(files: &mut Files, owner: u64, gen: u64, k: &Key, d: &[u8]) {
|
||||||
|
match k.ty {
|
||||||
|
INODE_ITEM if d.len() >= 160 => {
|
||||||
|
let e = files.entry((owner, k.objectid)).or_default();
|
||||||
|
let g = le64(d, 0);
|
||||||
|
if !e.has_inode || g >= e.gen {
|
||||||
|
e.has_inode = true;
|
||||||
|
e.gen = g;
|
||||||
|
e.size = le64(d, 16);
|
||||||
|
e.mode = le32(d, 52);
|
||||||
|
e.atime = le64(d, 112) as i64;
|
||||||
|
e.ctime = le64(d, 124) as i64;
|
||||||
|
e.mtime = le64(d, 136) as i64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
INODE_REF if d.len() >= 10 => {
|
||||||
|
let nlen = le16(d, 8) as usize;
|
||||||
|
if let Some(n) = d.get(10..10 + nlen) {
|
||||||
|
files
|
||||||
|
.entry((owner, k.objectid))
|
||||||
|
.or_default()
|
||||||
|
.refs
|
||||||
|
.insert(k.offset, String::from_utf8_lossy(n).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXTENT_DATA if d.len() >= 21 => {
|
||||||
|
let e = files.entry((owner, k.objectid)).or_default();
|
||||||
|
match e.extents.get(&k.offset) {
|
||||||
|
Some((g, _)) if *g > gen => {}
|
||||||
|
_ => {
|
||||||
|
e.extents.insert(k.offset, (gen, d.to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_fs_tree(id: u64) -> bool {
|
||||||
|
id == 5 || (256..(1u64 << 48)).contains(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
||||||
|
let _ = part_len;
|
||||||
|
let mut res = VolumeScan::default();
|
||||||
|
let Some(fs) = Fs::open(ctx.src, base) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
if fs
|
||||||
|
.chunks
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.ty & 0x1 << 3 != 0 || c.ty & 0x10 != 0 || c.ty & 0x100 != 0 || c.ty & 0x200 != 0)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Árvores atuais: raízes das subvolumes e extents alocados.
|
||||||
|
let mut roots: Vec<(u64, u64)> = vec![]; // (id da árvore, bytenr)
|
||||||
|
let mut extent_root = 0u64;
|
||||||
|
let rr = fs.root;
|
||||||
|
fs.walk(rr, &mut |k, d| {
|
||||||
|
if k.ty == ROOT_ITEM && d.len() >= 239 {
|
||||||
|
let bytenr = le64(d, 176);
|
||||||
|
if k.objectid == 2 {
|
||||||
|
extent_root = bytenr;
|
||||||
|
} else if is_fs_tree(k.objectid) {
|
||||||
|
roots.push((k.objectid, bytenr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut live_nodes: HashSet<u64> = HashSet::new();
|
||||||
|
let mut live: Files = HashMap::new();
|
||||||
|
for &(id, bytenr) in &roots {
|
||||||
|
let mut tmp: Vec<(Key, Vec<u8>)> = vec![];
|
||||||
|
let mut gens: HashMap<u64, u64> = HashMap::new();
|
||||||
|
let mut seen_nodes: Vec<(u64, u64)> = vec![];
|
||||||
|
{
|
||||||
|
let mut items = |k: &Key, d: &[u8]| tmp.push((*k, d.to_vec()));
|
||||||
|
let mut nodes = |n: &Node| {
|
||||||
|
seen_nodes.push((n.bytenr, n.generation));
|
||||||
|
};
|
||||||
|
fs.walk_nodes(bytenr, &mut items, &mut nodes);
|
||||||
|
}
|
||||||
|
for (b, g) in seen_nodes {
|
||||||
|
live_nodes.insert(b);
|
||||||
|
gens.insert(b, g);
|
||||||
|
}
|
||||||
|
for (k, d) in &tmp {
|
||||||
|
add_item(&mut live, id, u64::MAX, k, d);
|
||||||
|
}
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extents alocados (árvore de extents) e mapa livre dos blocos de dados.
|
||||||
|
let mut allocated: Vec<Extent> = vec![];
|
||||||
|
if extent_root != 0 {
|
||||||
|
let nodesize = fs.nodesize;
|
||||||
|
fs.walk(extent_root, &mut |k, _| {
|
||||||
|
if k.ty == EXTENT_ITEM {
|
||||||
|
allocated.push(Extent::new(k.objectid, k.offset));
|
||||||
|
} else if k.ty == METADATA_ITEM {
|
||||||
|
allocated.push(Extent::new(k.objectid, nodesize));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let allocated = merge_extents(allocated);
|
||||||
|
let mut free: Vec<Extent> = vec![];
|
||||||
|
for c in fs.chunks.iter().filter(|c| c.ty & 0x1 != 0) {
|
||||||
|
// blocos de dados: livres = chunk - alocado
|
||||||
|
let mut cur = c.logical;
|
||||||
|
let i = allocated.partition_point(|e| e.end() <= c.logical);
|
||||||
|
for e in &allocated[i..] {
|
||||||
|
if e.offset >= c.logical + c.len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if e.offset > cur {
|
||||||
|
if let Some(p) = fs.phys(cur) {
|
||||||
|
free.push(Extent::new(p, e.offset - cur));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur = cur.max(e.end());
|
||||||
|
}
|
||||||
|
if cur < c.logical + c.len {
|
||||||
|
if let Some(p) = fs.phys(cur) {
|
||||||
|
free.push(Extent::new(p, c.logical + c.len - cur));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.free = Some(merge_extents(free));
|
||||||
|
|
||||||
|
// 2) Varre os blocos de metadata atrás de versões antigas de folhas de árvores de arquivos.
|
||||||
|
let mut stale: Files = HashMap::new();
|
||||||
|
let ns = fs.nodesize 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 total_md: u64 = md_chunks.iter().map(|c| c.len).sum();
|
||||||
|
let mut done_md = 0u64;
|
||||||
|
for c in &md_chunks {
|
||||||
|
let Some(p0) = c.phys.first().copied() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut off = 0u64;
|
||||||
|
let block = 8usize << 20;
|
||||||
|
while off < c.len {
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
let n = ((c.len - off) as usize).min(block + ns);
|
||||||
|
let Ok(buf) = read_vec(ctx.src, fs.base + p0 + off, n) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let mut i = 0usize;
|
||||||
|
while i + ns <= buf.len() && (off + i as u64) < c.len {
|
||||||
|
let logical = c.logical + off + i as u64;
|
||||||
|
// cabeçalho barato antes do CRC
|
||||||
|
if buf[i + 32..i + 48] == fs.fsid
|
||||||
|
&& le64(&buf[i..], 48) == logical
|
||||||
|
&& buf[i + 100] == 0
|
||||||
|
{
|
||||||
|
if let Some(node) = fs.parse_node(&buf[i..i + ns], Some(logical)) {
|
||||||
|
if is_fs_tree(node.owner) && !live_nodes.contains(&logical) {
|
||||||
|
if let NodeKind::Leaf(items) = &node.kind {
|
||||||
|
for (k, d) in items {
|
||||||
|
add_item(&mut stale, node.owner, node.generation, k, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += step;
|
||||||
|
}
|
||||||
|
off += block as u64;
|
||||||
|
done_md += block as u64;
|
||||||
|
(ctx.progress)(done_md.min(total_md), total_md.max(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Arquivos que só existem nas folhas antigas.
|
||||||
|
let path_of = |owner: u64, mut ino: u64| -> String {
|
||||||
|
let mut parts: Vec<String> = vec![];
|
||||||
|
for _ in 0..64 {
|
||||||
|
if ino == 256 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let e = stale.get(&(owner, ino)).or_else(|| live.get(&(owner, ino)));
|
||||||
|
match e.and_then(|e| e.refs.iter().next()) {
|
||||||
|
Some((parent, name)) => {
|
||||||
|
parts.push(name.clone());
|
||||||
|
ino = *parent;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
parts.push("(pasta perdida)".into());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.reverse();
|
||||||
|
parts.join("/")
|
||||||
|
};
|
||||||
|
let mut keys: Vec<&(u64, u64)> = stale.keys().collect();
|
||||||
|
keys.sort();
|
||||||
|
for key in keys {
|
||||||
|
let (owner, ino) = *key;
|
||||||
|
if live.contains_key(&(owner, ino)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let e = &stale[&(owner, ino)];
|
||||||
|
if !e.has_inode || e.mode & 0o170000 != 0o100000 || e.size == 0 || e.extents.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some((name, parent)) = e.refs.iter().next().map(|(p, n)| (n.clone(), *p)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let dir = {
|
||||||
|
let p = path_of(owner, parent);
|
||||||
|
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 (conf, note) = if !all_ok {
|
||||||
|
(
|
||||||
|
Confidence::Low,
|
||||||
|
"faltam trechos do arquivo na versão antiga da árvore".to_string(),
|
||||||
|
)
|
||||||
|
} else if any_alloc {
|
||||||
|
(Confidence::Medium, "versão antiga do Btrfs; alguns blocos ainda constam como alocados (podem ser de outro arquivo)".to_string())
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Confidence::High,
|
||||||
|
"versão antiga da árvore do Btrfs; blocos de dados livres".to_string(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let ts = |t: i64| (t > 0).then_some(t);
|
||||||
|
let nf = NewFound {
|
||||||
|
name,
|
||||||
|
dir,
|
||||||
|
size: e.size,
|
||||||
|
origin: Origin::Btrfs,
|
||||||
|
data,
|
||||||
|
modified: ts(e.mtime),
|
||||||
|
created: ts(e.ctime),
|
||||||
|
accessed: ts(e.atime),
|
||||||
|
base: conf,
|
||||||
|
note: Some(¬e),
|
||||||
|
};
|
||||||
|
let f = make_found(ctx, nf);
|
||||||
|
(ctx.emit)(f);
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converte os itens EXTENT_DATA em extents físicos ou partes (inline/comprimidas).
|
||||||
|
fn build_data(fs: &Fs, e: &InodeData, allocated: &[Extent]) -> Option<(DataLoc, bool, bool)> {
|
||||||
|
let mut parts: Vec<Part> = vec![];
|
||||||
|
let mut pos = 0u64;
|
||||||
|
let mut any_alloc = false;
|
||||||
|
let mut all_ok = true;
|
||||||
|
let mut plain_only = true;
|
||||||
|
let mut hole = |parts: &mut Vec<Part>, len: u64| {
|
||||||
|
if len > 0 {
|
||||||
|
parts.push(Part::Run { offset: 0, len });
|
||||||
|
// marcador: offset 0 + sparse tratado abaixo
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = &mut hole;
|
||||||
|
let mut sparse_marks: Vec<usize> = vec![];
|
||||||
|
for (&foff, (_, d)) in &e.extents {
|
||||||
|
if foff >= e.size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if foff > pos {
|
||||||
|
sparse_marks.push(parts.len());
|
||||||
|
parts.push(Part::Run {
|
||||||
|
offset: 0,
|
||||||
|
len: foff - pos,
|
||||||
|
});
|
||||||
|
plain_only = false;
|
||||||
|
pos = foff;
|
||||||
|
}
|
||||||
|
let ty = d[20];
|
||||||
|
let comp = d[16];
|
||||||
|
if ty == 0 {
|
||||||
|
// inline
|
||||||
|
let raw = &d[21..];
|
||||||
|
let ram = le64(d, 8) as usize;
|
||||||
|
let bytes = match comp {
|
||||||
|
0 => raw.to_vec(),
|
||||||
|
c => decode(c, raw, ram)?,
|
||||||
|
};
|
||||||
|
let len = (bytes.len() as u64).min(e.size - foff);
|
||||||
|
parts.push(Part::Bytes {
|
||||||
|
bytes: bytes[..len as usize].to_vec(),
|
||||||
|
});
|
||||||
|
plain_only = false;
|
||||||
|
pos = foff + len;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if d.len() < 53 {
|
||||||
|
all_ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let (disk, disk_len, offset, num) = (le64(d, 21), le64(d, 29), le64(d, 37), le64(d, 45));
|
||||||
|
let num = num.min(e.size.saturating_sub(foff));
|
||||||
|
if disk == 0 || ty == 2 {
|
||||||
|
sparse_marks.push(parts.len());
|
||||||
|
parts.push(Part::Run {
|
||||||
|
offset: 0,
|
||||||
|
len: num,
|
||||||
|
});
|
||||||
|
plain_only = false;
|
||||||
|
} else {
|
||||||
|
let Some(phys) = fs.phys(disk) else {
|
||||||
|
all_ok = false;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let overlaps = {
|
||||||
|
let i = allocated.partition_point(|x| x.end() <= disk);
|
||||||
|
allocated.get(i).is_some_and(|x| x.offset < disk + disk_len)
|
||||||
|
};
|
||||||
|
any_alloc |= overlaps;
|
||||||
|
if comp == 0 {
|
||||||
|
parts.push(Part::Run {
|
||||||
|
offset: phys + offset,
|
||||||
|
len: num,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let codec = match comp {
|
||||||
|
1 => PackedCodec::Zlib,
|
||||||
|
2 => PackedCodec::Zstd,
|
||||||
|
3..=6 => PackedCodec::Lzo,
|
||||||
|
_ => {
|
||||||
|
all_ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
parts.push(Part::Packed(Packed {
|
||||||
|
offset: phys,
|
||||||
|
disk_len,
|
||||||
|
ram_len: le64(d, 8),
|
||||||
|
skip: offset,
|
||||||
|
len: num,
|
||||||
|
codec,
|
||||||
|
}));
|
||||||
|
plain_only = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos = foff + num;
|
||||||
|
}
|
||||||
|
if pos < e.size {
|
||||||
|
all_ok = false;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
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.
|
||||||
|
let mut out: Vec<Part> = vec![];
|
||||||
|
for (i, p) in parts.into_iter().enumerate() {
|
||||||
|
if sparse_marks.contains(&i) {
|
||||||
|
if let Part::Run { len, .. } = p {
|
||||||
|
out.push(Part::Zeros { len });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(p);
|
||||||
|
}
|
||||||
|
Some((DataLoc::Parts { parts: out }, any_alloc, all_ok))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode(codec: u8, raw: &[u8], ram: usize) -> Option<Vec<u8>> {
|
||||||
|
match codec {
|
||||||
|
1 => {
|
||||||
|
use std::io::Read;
|
||||||
|
let mut v = Vec::with_capacity(ram);
|
||||||
|
flate2::read::ZlibDecoder::new(raw)
|
||||||
|
.read_to_end(&mut v)
|
||||||
|
.ok()?;
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
use std::io::Read;
|
||||||
|
let mut dec = ruzstd::StreamingDecoder::new(raw).ok()?;
|
||||||
|
let mut v = Vec::with_capacity(ram);
|
||||||
|
dec.read_to_end(&mut v).ok()?;
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
_ => lzo_btrfs(raw, ram),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formato de extensão LZO do Btrfs: tamanho total (u32) + segmentos [tamanho (u32)][dados LZO1X]
|
||||||
|
/// de até um setor de 4 KiB descomprimido cada, sem cruzar fronteiras de setor.
|
||||||
|
pub fn lzo_btrfs(raw: &[u8], ram: usize) -> Option<Vec<u8>> {
|
||||||
|
if raw.len() < 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let total = le32(raw, 0) as usize;
|
||||||
|
let mut out = Vec::with_capacity(ram);
|
||||||
|
let mut p = 4usize;
|
||||||
|
while p + 4 <= raw.len().min(total) && out.len() < ram {
|
||||||
|
// pula o preenchimento até o próximo setor se sobrar menos de 4 bytes
|
||||||
|
let in_sector = 4096 - (p % 4096);
|
||||||
|
if in_sector < 4 {
|
||||||
|
p += in_sector;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let seg = le32(raw, p) as usize;
|
||||||
|
p += 4;
|
||||||
|
let chunk = raw.get(p..p + seg)?;
|
||||||
|
let want = (ram - out.len()).min(4096);
|
||||||
|
out.extend(lzo1x_decompress(chunk, want)?);
|
||||||
|
p += seg;
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// LZO1X (descompressão segura, no mesmo esquema de estados do kernel Linux).
|
||||||
|
pub fn lzo1x_decompress(src: &[u8], max_out: usize) -> Option<Vec<u8>> {
|
||||||
|
let mut out: Vec<u8> = Vec::with_capacity(max_out);
|
||||||
|
let mut ip = 0usize;
|
||||||
|
let byte = |i: usize| src.get(i).copied().map(|b| b as usize);
|
||||||
|
let lits = |out: &mut Vec<u8>, ip: &mut usize, n: usize| -> Option<()> {
|
||||||
|
out.extend_from_slice(src.get(*ip..*ip + n)?);
|
||||||
|
*ip += n;
|
||||||
|
Some(())
|
||||||
|
};
|
||||||
|
let ext = |ip: &mut usize, base: usize| -> Option<usize> {
|
||||||
|
let mut extra = 0usize;
|
||||||
|
while byte(*ip)? == 0 {
|
||||||
|
extra += 255;
|
||||||
|
*ip += 1;
|
||||||
|
}
|
||||||
|
let v = base + extra + byte(*ip)?;
|
||||||
|
*ip += 1;
|
||||||
|
Some(v)
|
||||||
|
};
|
||||||
|
// estado: literais ao fim da instrução anterior (0..3) ou 4 depois de uma corrida de literais
|
||||||
|
let mut state;
|
||||||
|
if byte(0)? > 17 {
|
||||||
|
let t = byte(0)? - 17;
|
||||||
|
ip = 1;
|
||||||
|
lits(&mut out, &mut ip, t)?;
|
||||||
|
state = if t < 4 { t } else { 4 };
|
||||||
|
} else {
|
||||||
|
state = 0;
|
||||||
|
}
|
||||||
|
loop {
|
||||||
|
if out.len() > max_out + 4096 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let t = byte(ip)?;
|
||||||
|
ip += 1;
|
||||||
|
if t < 16 {
|
||||||
|
if state == 0 {
|
||||||
|
let n = if t == 0 { ext(&mut ip, 15)? } else { t } + 3;
|
||||||
|
lits(&mut out, &mut ip, n)?;
|
||||||
|
state = 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let h = byte(ip)?;
|
||||||
|
ip += 1;
|
||||||
|
let (dist, len) = if state == 4 {
|
||||||
|
((t >> 2) + (h << 2) + 2049, 3)
|
||||||
|
} else {
|
||||||
|
((t >> 2) + (h << 2) + 1, 2)
|
||||||
|
};
|
||||||
|
copy_match(&mut out, dist, len)?;
|
||||||
|
let next = t & 3;
|
||||||
|
lits(&mut out, &mut ip, next)?;
|
||||||
|
state = next;
|
||||||
|
} else if t >= 64 {
|
||||||
|
let h = byte(ip)?;
|
||||||
|
ip += 1;
|
||||||
|
copy_match(&mut out, 1 + ((t >> 2) & 7) + (h << 3), (t >> 5) + 1)?;
|
||||||
|
let next = t & 3;
|
||||||
|
lits(&mut out, &mut ip, next)?;
|
||||||
|
state = next;
|
||||||
|
} else if t >= 32 {
|
||||||
|
let len = if t & 31 == 0 {
|
||||||
|
ext(&mut ip, 31)?
|
||||||
|
} else {
|
||||||
|
t & 31
|
||||||
|
} + 2;
|
||||||
|
let b = (byte(ip)?) | (byte(ip + 1)? << 8);
|
||||||
|
ip += 2;
|
||||||
|
copy_match(&mut out, 1 + (b >> 2), len)?;
|
||||||
|
let next = b & 3;
|
||||||
|
lits(&mut out, &mut ip, next)?;
|
||||||
|
state = next;
|
||||||
|
} else {
|
||||||
|
let len = if t & 7 == 0 { ext(&mut ip, 7)? } else { t & 7 } + 2;
|
||||||
|
let b = (byte(ip)?) | (byte(ip + 1)? << 8);
|
||||||
|
ip += 2;
|
||||||
|
let d = ((t & 8) << 11) + (b >> 2);
|
||||||
|
if d == 0 {
|
||||||
|
return Some(out);
|
||||||
|
}
|
||||||
|
copy_match(&mut out, d + 0x4000, len)?;
|
||||||
|
let next = b & 3;
|
||||||
|
lits(&mut out, &mut ip, next)?;
|
||||||
|
state = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_match(out: &mut Vec<u8>, dist: usize, len: usize) -> Option<()> {
|
||||||
|
if dist == 0 || dist > out.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let start = out.len() - dist;
|
||||||
|
for i in 0..len {
|
||||||
|
let b = out[start + i];
|
||||||
|
out.push(b);
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arquivos e pastas atuais com caminho completo e tamanho (usa só as árvores vivas).
|
||||||
|
pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
|
||||||
|
let Some(fs) = Fs::open(src, base) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let mut roots: Vec<(u64, u64)> = vec![];
|
||||||
|
fs.walk(fs.root, &mut |k, d| {
|
||||||
|
if k.ty == ROOT_ITEM && d.len() >= 239 && is_fs_tree(k.objectid) {
|
||||||
|
roots.push((k.objectid, le64(d, 176)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut files: Files = HashMap::new();
|
||||||
|
for (id, b) in roots {
|
||||||
|
fs.walk(b, &mut |k, d| add_item(&mut files, id, u64::MAX, k, d));
|
||||||
|
}
|
||||||
|
let mut out = vec![];
|
||||||
|
for (&(owner, ino), e) in &files {
|
||||||
|
if !e.has_inode || ino == 256 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut parts = vec![];
|
||||||
|
let mut cur = ino;
|
||||||
|
for _ in 0..64 {
|
||||||
|
if cur == 256 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match files.get(&(owner, cur)).and_then(|x| x.refs.iter().next()) {
|
||||||
|
Some((p, n)) => {
|
||||||
|
parts.push(n.clone());
|
||||||
|
cur = *p;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.reverse();
|
||||||
|
out.push((parts.join("/"), e.size, e.mode & 0o170000 == 0o040000));
|
||||||
|
}
|
||||||
|
out.sort();
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Posições das árvores principais, para ferramentas e testes que precisam editar a imagem.
|
||||||
|
pub struct TreeLocations {
|
||||||
|
pub nodesize: u64,
|
||||||
|
/// Endereço físico (absoluto na origem) do primeiro nó de cada árvore.
|
||||||
|
pub root_tree: u64,
|
||||||
|
pub fs_tree: u64,
|
||||||
|
pub extent_tree: u64,
|
||||||
|
/// Endereços lógicos correspondentes.
|
||||||
|
pub root_tree_logical: u64,
|
||||||
|
pub fs_tree_logical: u64,
|
||||||
|
pub extent_tree_logical: u64,
|
||||||
|
/// Converte endereço lógico em físico.
|
||||||
|
pub chunks: Vec<(u64, u64, u64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn locate_trees(src: &dyn Source, base: u64) -> Option<TreeLocations> {
|
||||||
|
let fs = Fs::open(src, base)?;
|
||||||
|
let (mut fs_t, mut ext_t) = (0u64, 0u64);
|
||||||
|
fs.walk(fs.root, &mut |k, d| {
|
||||||
|
if k.ty == ROOT_ITEM && d.len() >= 239 {
|
||||||
|
if k.objectid == 5 {
|
||||||
|
fs_t = le64(d, 176);
|
||||||
|
} else if k.objectid == 2 {
|
||||||
|
ext_t = le64(d, 176);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Some(TreeLocations {
|
||||||
|
nodesize: fs.nodesize,
|
||||||
|
root_tree: fs.phys(fs.root)?,
|
||||||
|
fs_tree: fs.phys(fs_t)?,
|
||||||
|
extent_tree: fs.phys(ext_t)?,
|
||||||
|
root_tree_logical: fs.root,
|
||||||
|
fs_tree_logical: fs_t,
|
||||||
|
extent_tree_logical: ext_t,
|
||||||
|
chunks: fs
|
||||||
|
.chunks
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| c.phys.first().map(|p| (c.logical, c.len, fs.base + p)))
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
//! HFS+ / HFSX (inclusive volumes embutidos num wrapper HFS clássico).
|
||||||
|
//!
|
||||||
|
//! Lê o catálogo (árvore B+) para nomes e caminhos, o bitmap de alocação para o mapa de
|
||||||
|
//! espaço livre e procura arquivos apagados em três lugares: sobras dentro dos nós folha do
|
||||||
|
//! catálogo, cópias antigas de nós no diário (journal) e registros de arquivo "soltos".
|
||||||
|
|
||||||
|
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use crate::types::{Confidence, DataLoc, Extent, Origin};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
const HFS_EPOCH: i64 = 2_082_844_800;
|
||||||
|
|
||||||
|
fn be16(b: &[u8], o: usize) -> u16 {
|
||||||
|
b.get(o..o + 2)
|
||||||
|
.map(|s| u16::from_be_bytes([s[0], s[1]]))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct Fork {
|
||||||
|
size: u64,
|
||||||
|
blocks: u32,
|
||||||
|
extents: Vec<(u32, u32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_fork(b: &[u8]) -> Fork {
|
||||||
|
let mut f = Fork {
|
||||||
|
size: be64(b, 0),
|
||||||
|
blocks: be32(b, 12),
|
||||||
|
extents: vec![],
|
||||||
|
};
|
||||||
|
for i in 0..8 {
|
||||||
|
let (s, c) = (be32(b, 16 + i * 8), be32(b, 20 + i * 8));
|
||||||
|
if c != 0 {
|
||||||
|
f.extents.push((s, c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Posição do volume HFS+ dentro da partição (wrapper HFS clássico) ou `None` se não for HFS+.
|
||||||
|
pub fn locate(src: &dyn Source, base: u64) -> Option<u64> {
|
||||||
|
let h = read_vec(src, base + 1024, 512).ok()?;
|
||||||
|
if h.len() < 512 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match &h[0..2] {
|
||||||
|
b"H+" | b"HX" => Some(base),
|
||||||
|
b"BD" if &h[124..126] == b"H+" => {
|
||||||
|
let alblk = be32(&h, 20) as u64;
|
||||||
|
let alblst = be16(&h, 28) as u64;
|
||||||
|
let start = be16(&h, 126) as u64;
|
||||||
|
Some(base + alblst * 512 + start * alblk)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Vol<'a> {
|
||||||
|
src: &'a dyn Source,
|
||||||
|
base: u64,
|
||||||
|
bs: u64,
|
||||||
|
total: u64,
|
||||||
|
catalog: Fork,
|
||||||
|
extents_file: Fork,
|
||||||
|
alloc: Fork,
|
||||||
|
next_cnid: u32,
|
||||||
|
journal_info: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Vol<'a> {
|
||||||
|
fn open(src: &'a dyn Source, base: u64) -> Option<Vol<'a>> {
|
||||||
|
let h = read_vec(src, base + 1024, 512).ok()?;
|
||||||
|
if h.len() < 512 || !(&h[0..2] == b"H+" || &h[0..2] == b"HX") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bs = be32(&h, 40) as u64;
|
||||||
|
if bs < 512 || !bs.is_power_of_two() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Vol {
|
||||||
|
src,
|
||||||
|
base,
|
||||||
|
bs,
|
||||||
|
total: be32(&h, 44) as u64,
|
||||||
|
alloc: parse_fork(&h[112..192]),
|
||||||
|
extents_file: parse_fork(&h[192..272]),
|
||||||
|
catalog: parse_fork(&h[272..352]),
|
||||||
|
next_cnid: be32(&h, 64),
|
||||||
|
journal_info: be32(&h, 12),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_off(&self, b: u64) -> u64 {
|
||||||
|
self.base + b * self.bs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Todas as extensões de um fork (inclusive as do arquivo de overflow).
|
||||||
|
fn fork_extents(&self, f: &Fork, file_id: u32, fork_type: u8) -> Vec<Extent> {
|
||||||
|
let mut ex: Vec<(u32, u32)> = f.extents.clone();
|
||||||
|
let mut have: u64 = ex.iter().map(|e| e.1 as u64).sum();
|
||||||
|
if have < f.blocks as u64 && file_id >= 2 {
|
||||||
|
ex.extend(self.overflow_extents(file_id, fork_type, have as u32));
|
||||||
|
have = ex.iter().map(|e| e.1 as u64).sum();
|
||||||
|
let _ = have;
|
||||||
|
}
|
||||||
|
merge_extents(
|
||||||
|
ex.into_iter()
|
||||||
|
.map(|(s, c)| Extent::new(self.block_off(s as u64), c as u64 * self.bs))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extensões além das oito iniciais, procuradas no arquivo de overflow (árvore B+).
|
||||||
|
fn overflow_extents(&self, file_id: u32, fork_type: u8, mut start: u32) -> Vec<(u32, u32)> {
|
||||||
|
let mut out = vec![];
|
||||||
|
let node_size = match self.tree_node_size(&self.extents_file) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => return out,
|
||||||
|
};
|
||||||
|
let total_nodes = self.extents_file.size / node_size as u64;
|
||||||
|
for n in 1..total_nodes.min(100_000) {
|
||||||
|
let Some(node) = self.read_tree_node(&self.extents_file, n as u32, node_size) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if node[8] != 0xFF {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for r in records(&node) {
|
||||||
|
// chave: keyLength u16, forkType u8, pad u8, fileID u32, startBlock u32
|
||||||
|
if r.len() < 12 + 64 || be16(r, 0) != 10 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if r[2] == fork_type && be32(r, 4) == file_id && be32(r, 8) == start {
|
||||||
|
let mut got = 0;
|
||||||
|
for i in 0..8 {
|
||||||
|
let (s, c) = (be32(r, 12 + i * 8), be32(r, 16 + i * 8));
|
||||||
|
if c != 0 {
|
||||||
|
out.push((s, c));
|
||||||
|
got += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start += got;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tree_node_size(&self, f: &Fork) -> Option<usize> {
|
||||||
|
let first = self.read_fork_bytes(f, 0, 64)?;
|
||||||
|
let ns = be16(&first, 32) as usize;
|
||||||
|
(512..=65536).contains(&ns).then_some(ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_tree_node(&self, f: &Fork, n: u32, ns: usize) -> Option<Vec<u8>> {
|
||||||
|
self.read_fork_bytes(f, n as u64 * ns as u64, ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lê bytes de um fork que tem só extensões iniciais ou overflow já resolvido (para árvores).
|
||||||
|
fn read_fork_bytes(&self, f: &Fork, off: u64, len: usize) -> Option<Vec<u8>> {
|
||||||
|
let mut out = Vec::with_capacity(len);
|
||||||
|
let mut pos = 0u64;
|
||||||
|
for &(s, c) in &f.extents {
|
||||||
|
let elen = c as u64 * self.bs;
|
||||||
|
if off + out.len() as u64 >= pos && off + (out.len() as u64) < pos + elen {
|
||||||
|
let within = off + out.len() as u64 - pos;
|
||||||
|
let n = ((elen - within) as usize).min(len - out.len());
|
||||||
|
out.extend(read_vec(self.src, self.block_off(s as u64) + within, n).ok()?);
|
||||||
|
if out.len() == len {
|
||||||
|
return Some(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos += elen;
|
||||||
|
}
|
||||||
|
(out.len() == len).then_some(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registros de um nó B+ (fatias, da chave em diante).
|
||||||
|
fn records(node: &[u8]) -> Vec<&[u8]> {
|
||||||
|
let n = be16(node, 10) as usize;
|
||||||
|
let mut offs = vec![];
|
||||||
|
for i in 0..=n {
|
||||||
|
let p = node.len().saturating_sub(2 * (i + 1));
|
||||||
|
offs.push(be16(node, p) as usize);
|
||||||
|
}
|
||||||
|
let mut out = vec![];
|
||||||
|
for i in 0..n {
|
||||||
|
let (a, b) = (offs[i], offs[i + 1]);
|
||||||
|
if a >= 14 && b > a && b <= node.len() {
|
||||||
|
out.push(&node[a..b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn utf16be(b: &[u8]) -> String {
|
||||||
|
let u: Vec<u16> = b
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
||||||
|
.collect();
|
||||||
|
String::from_utf16_lossy(&u)
|
||||||
|
.replace('/', "_")
|
||||||
|
.replace(':', "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CatFile {
|
||||||
|
parent: u32,
|
||||||
|
name: String,
|
||||||
|
id: u32,
|
||||||
|
data: Fork,
|
||||||
|
created: u32,
|
||||||
|
modified: u32,
|
||||||
|
accessed: u32,
|
||||||
|
stale: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn date(d: u32) -> Option<i64> {
|
||||||
|
(d != 0).then(|| d as i64 - HFS_EPOCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
) {
|
||||||
|
let klen = be16(r, 0) as usize;
|
||||||
|
if klen < 6 || klen + 2 > r.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let parent = be32(r, 2);
|
||||||
|
let nlen = be16(r, 6) as usize;
|
||||||
|
if nlen > 255 || klen != 6 + nlen * 2 && klen != 6 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let name = utf16be(r.get(8..8 + nlen * 2).unwrap_or(&[]));
|
||||||
|
let mut d = 2 + klen;
|
||||||
|
if d % 2 == 1 {
|
||||||
|
d += 1;
|
||||||
|
}
|
||||||
|
let Some(rec) = r.get(d..) else { return };
|
||||||
|
match be16(rec, 0) {
|
||||||
|
1 if rec.len() >= 88 => {
|
||||||
|
let id = be32(rec, 8);
|
||||||
|
if id >= 2 && id < max_id.max(16) {
|
||||||
|
folders.entry((parent, name)).or_insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 if rec.len() >= 10 => {
|
||||||
|
// thread de pasta: chave = (id da pasta, ""); dados = (pai, nome)
|
||||||
|
let pn = be16(rec, 8) as usize;
|
||||||
|
if pn <= 255 {
|
||||||
|
threads.entry(parent).or_insert((
|
||||||
|
be32(rec, 4),
|
||||||
|
utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[])),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 if rec.len() >= 248 => {
|
||||||
|
let id = be32(rec, 8);
|
||||||
|
if id < 16 || id >= max_id.max(16) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = parse_fork(&rec[88..168]);
|
||||||
|
files.push(CatFile {
|
||||||
|
parent,
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
created: be32(rec, 12),
|
||||||
|
modified: be32(rec, 16),
|
||||||
|
accessed: be32(rec, 24),
|
||||||
|
stale,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
4 if rec.len() >= 10 => {
|
||||||
|
let pn = be16(rec, 8) as usize;
|
||||||
|
if pn <= 255 {
|
||||||
|
threads.entry(parent).or_insert((
|
||||||
|
be32(rec, 4),
|
||||||
|
utf16be(rec.get(10..10 + pn * 2).unwrap_or(&[])),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
) {
|
||||||
|
let n = be16(node, 10) as usize;
|
||||||
|
let table = node.len().saturating_sub(2 * (n + 1));
|
||||||
|
let used_end = (be16(node, table) as usize).max(14);
|
||||||
|
let mut p = used_end + (used_end % 2);
|
||||||
|
while p + 40 < table {
|
||||||
|
let klen = be16(node, p) as usize;
|
||||||
|
let nlen = be16(node, p + 6) as usize;
|
||||||
|
if klen == 6 + nlen * 2 && (1..=255).contains(&nlen) && p + 2 + klen + 2 < table {
|
||||||
|
let parent = be32(node, p + 2);
|
||||||
|
if parent >= 1 && parent < max_id.max(16) {
|
||||||
|
let d = (p + 2 + klen + 1) & !1;
|
||||||
|
let rt = be16(node, d);
|
||||||
|
if matches!(rt, 1..=4) {
|
||||||
|
let end = (d + 248).min(table);
|
||||||
|
decode_record(&node[p..end], max_id, files, threads, folders, true);
|
||||||
|
p = d + if rt == 2 { 248 } else { 16 };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
||||||
|
let mut res = VolumeScan::default();
|
||||||
|
let _ = part_len;
|
||||||
|
let Some(vbase) = locate(ctx.src, base) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
let Some(vol) = Vol::open(ctx.src, vbase) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapa de espaço livre.
|
||||||
|
if let Some(bm) = vol.read_fork_all(&vol.alloc, (vol.total / 8 + 1) as usize) {
|
||||||
|
let mut free = vec![];
|
||||||
|
let mut run: Option<u64> = None;
|
||||||
|
for b in 0..vol.total {
|
||||||
|
let used = bm
|
||||||
|
.get((b / 8) as usize)
|
||||||
|
.map(|x| x >> (7 - (b % 8)) & 1 == 1)
|
||||||
|
.unwrap_or(true);
|
||||||
|
match (used, run) {
|
||||||
|
(false, None) => run = Some(b),
|
||||||
|
(true, Some(s)) => {
|
||||||
|
free.push(Extent::new(vol.block_off(s), (b - s) * vol.bs));
|
||||||
|
run = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(s) = run {
|
||||||
|
free.push(Extent::new(vol.block_off(s), (vol.total - s) * vol.bs));
|
||||||
|
}
|
||||||
|
res.free = Some(merge_extents(free));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(ns) = vol.tree_node_size(&vol.catalog) else {
|
||||||
|
res.warnings.push("HFS+: catálogo ilegível.".into());
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
let Some(hdr) = vol.read_tree_node(&vol.catalog, 0, ns) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
let first_leaf = be32(&hdr, 24);
|
||||||
|
let max_id = vol.next_cnid.max(16);
|
||||||
|
let mut files: Vec<CatFile> = vec![];
|
||||||
|
let mut threads: HashMap<u32, (u32, String)> = HashMap::new();
|
||||||
|
let mut folders: HashMap<(u32, String), u32> = HashMap::new();
|
||||||
|
let mut live_ids: HashSet<u32> = HashSet::new();
|
||||||
|
|
||||||
|
let mut n = first_leaf;
|
||||||
|
let mut guard = 0;
|
||||||
|
while n != 0 && guard < 5_000_000 {
|
||||||
|
guard += 1;
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
let Some(node) = vol.read_tree_node(&vol.catalog, n, ns) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if node[8] != 0xFF {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut live_here: Vec<CatFile> = vec![];
|
||||||
|
for r in records(&node) {
|
||||||
|
decode_record(r, max_id, &mut live_here, &mut threads, &mut folders, false);
|
||||||
|
}
|
||||||
|
for f in &live_here {
|
||||||
|
live_ids.insert(f.id);
|
||||||
|
}
|
||||||
|
files.extend(live_here);
|
||||||
|
scan_slack(&node, max_id, &mut files, &mut threads, &mut folders);
|
||||||
|
n = be32(&node, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diário: cópias antigas de nós folha do catálogo.
|
||||||
|
if vol.journal_info != 0 {
|
||||||
|
scan_journal(
|
||||||
|
&vol,
|
||||||
|
ns,
|
||||||
|
max_id,
|
||||||
|
&mut files,
|
||||||
|
&mut threads,
|
||||||
|
&mut folders,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caminho de cada pasta a partir dos threads.
|
||||||
|
let path_of = |parent: u32| -> String {
|
||||||
|
let mut parts = vec![];
|
||||||
|
let mut cur = parent;
|
||||||
|
for _ in 0..64 {
|
||||||
|
if cur <= 2 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match threads.get(&cur) {
|
||||||
|
Some((p, name)) => {
|
||||||
|
parts.push(name.clone());
|
||||||
|
cur = *p;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.reverse();
|
||||||
|
format!("/{}", parts.join("/"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let free_set = res.free.clone().unwrap_or_default();
|
||||||
|
let mut seen: HashSet<(u32, u64)> = HashSet::new();
|
||||||
|
for f in files
|
||||||
|
.iter()
|
||||||
|
.filter(|f| f.stale && !live_ids.contains(&f.id))
|
||||||
|
{
|
||||||
|
if f.data.size == 0 || f.data.extents.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ex = vol.fork_extents(&f.data, f.id, 0);
|
||||||
|
let total_len: u64 = ex.iter().map(|e| e.len).sum();
|
||||||
|
if ex.is_empty()
|
||||||
|
|| total_len < f.data.size
|
||||||
|
|| ex.iter().any(|e| e.end() > vol.base + vol.total * vol.bs)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !seen.insert((f.id, ex[0].offset)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Mantém só os bytes do arquivo.
|
||||||
|
let extents = super::fat::trim_extents(ex, f.data.size);
|
||||||
|
// Blocos já reaproveitados?
|
||||||
|
let reused = extents.iter().filter(|e| !covered_by(&free_set, e)).count();
|
||||||
|
let (conf, note) = if reused == 0 {
|
||||||
|
(
|
||||||
|
Confidence::High,
|
||||||
|
"registro apagado do catálogo HFS+; blocos ainda livres".to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Confidence::Low,
|
||||||
|
"alguns blocos do arquivo já foram reaproveitados".to_string(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let nf = NewFound {
|
||||||
|
name: f.name.clone(),
|
||||||
|
dir: Some(path_of(f.parent)),
|
||||||
|
size: f.data.size,
|
||||||
|
origin: Origin::Hfs,
|
||||||
|
data: DataLoc::Extents { extents },
|
||||||
|
modified: date(f.modified),
|
||||||
|
created: date(f.created),
|
||||||
|
accessed: date(f.accessed),
|
||||||
|
base: conf,
|
||||||
|
note: Some(¬e),
|
||||||
|
};
|
||||||
|
let found = make_found(ctx, nf);
|
||||||
|
(ctx.emit)(found);
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn covered_by(free: &[Extent], e: &Extent) -> bool {
|
||||||
|
let i = free.partition_point(|f| f.end() <= e.offset);
|
||||||
|
free.get(i)
|
||||||
|
.map(|f| f.offset <= e.offset && f.end() >= e.end())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vol<'_> {
|
||||||
|
fn read_fork_all(&self, f: &Fork, max: usize) -> Option<Vec<u8>> {
|
||||||
|
let mut out = vec![];
|
||||||
|
for &(s, c) in &f.extents {
|
||||||
|
let n = (c as u64 * self.bs) as usize;
|
||||||
|
out.extend(
|
||||||
|
read_vec(
|
||||||
|
self.src,
|
||||||
|
self.block_off(s as u64),
|
||||||
|
n.min(max.saturating_sub(out.len())),
|
||||||
|
)
|
||||||
|
.ok()?,
|
||||||
|
);
|
||||||
|
if out.len() >= max {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
) {
|
||||||
|
let Some(info) = read_vec(vol.src, vol.block_off(vol.journal_info as u64), 52)
|
||||||
|
.ok()
|
||||||
|
.filter(|b| b.len() == 52)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (joff, jsize) = (be64(&info, 36), be64(&info, 44));
|
||||||
|
if jsize == 0 || jsize > 1 << 30 || vol.base + joff + jsize > vol.src.size() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut pos = 0u64;
|
||||||
|
let chunk = 4 << 20;
|
||||||
|
while pos < jsize {
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let n = chunk.min(jsize - pos) as usize;
|
||||||
|
let Ok(buf) = read_vec(vol.src, vol.base + joff + pos, n + ns) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let mut i = 0;
|
||||||
|
while i + ns <= buf.len().min(n + ns) && i < n {
|
||||||
|
let nd = &buf[i..i + ns];
|
||||||
|
// nó folha do catálogo: kind = -1, altura 1, registros plausíveis
|
||||||
|
if nd[8] == 0xFF && nd[9] == 1 && be16(nd, 10) > 0 && (be16(nd, 10) as usize) < ns / 8 {
|
||||||
|
let table = ns - 2 * (be16(nd, 10) as usize + 1);
|
||||||
|
if (be16(nd, ns - 2) as usize) == 14
|
||||||
|
&& be16(nd, table) as usize > 14
|
||||||
|
&& (be16(nd, table) as usize) <= table
|
||||||
|
{
|
||||||
|
for r in records(nd) {
|
||||||
|
decode_record(r, max_id, files, threads, folders, true);
|
||||||
|
}
|
||||||
|
scan_slack(nd, max_id, files, threads, folders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 512;
|
||||||
|
}
|
||||||
|
pos += n as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,14 @@
|
|||||||
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
|
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
|
||||||
//! possível, os trechos livres.
|
//! possível, os trechos livres.
|
||||||
|
|
||||||
|
pub mod apfs;
|
||||||
|
pub mod btrfs;
|
||||||
pub mod exfat;
|
pub mod exfat;
|
||||||
pub mod ext;
|
pub mod ext;
|
||||||
pub mod fat;
|
pub mod fat;
|
||||||
|
pub mod hfsplus;
|
||||||
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};
|
||||||
@@ -65,6 +69,7 @@ pub fn make_found(ctx: &Ctx, n: NewFound) -> Found {
|
|||||||
let mut note = n.note.map(String::from);
|
let mut note = n.note.map(String::from);
|
||||||
let head: Vec<u8> = match &n.data {
|
let head: Vec<u8> = match &n.data {
|
||||||
DataLoc::Resident { bytes } => bytes.iter().take(32).copied().collect(),
|
DataLoc::Resident { bytes } => bytes.iter().take(32).copied().collect(),
|
||||||
|
DataLoc::Parts { .. } => vec![],
|
||||||
DataLoc::Extents { extents } => extents
|
DataLoc::Extents { extents } => extents
|
||||||
.iter()
|
.iter()
|
||||||
.find(|e| !e.sparse)
|
.find(|e| !e.sparse)
|
||||||
|
|||||||
@@ -0,0 +1,550 @@
|
|||||||
|
//! XFS (v4 e v5): mapa de espaço livre pela árvore de blocos livres de cada grupo de alocação e
|
||||||
|
//! arquivos apagados. Ao liberar um inode, o XFS zera o modo e o tamanho mas não apaga a lista de
|
||||||
|
//! extents que ficou dentro do inode; e a entrada de diretório liberada perde só os 4 primeiros
|
||||||
|
//! bytes (o número do inode), preservando o nome. Juntamos as duas sobras.
|
||||||
|
|
||||||
|
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
|
||||||
|
use crate::signatures;
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use crate::types::{Confidence, DataLoc, Extent, Origin};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
fn be16(b: &[u8], o: usize) -> u16 {
|
||||||
|
b.get(o..o + 2)
|
||||||
|
.map(|s| u16::from_be_bytes([s[0], s[1]]))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Xfs<'a> {
|
||||||
|
src: &'a dyn Source,
|
||||||
|
base: u64,
|
||||||
|
bs: u64,
|
||||||
|
agblocks: u64,
|
||||||
|
agcount: u64,
|
||||||
|
inodesize: u64,
|
||||||
|
inopblog: u32,
|
||||||
|
agblklog: u32,
|
||||||
|
sectsize: u64,
|
||||||
|
v5: bool,
|
||||||
|
ftype: bool,
|
||||||
|
root_ino: u64,
|
||||||
|
dblocks: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Xfs<'a> {
|
||||||
|
fn open(src: &'a dyn Source, base: u64) -> Option<Xfs<'a>> {
|
||||||
|
let sb = read_vec(src, base, 512).ok()?;
|
||||||
|
if sb.len() < 512 || &sb[0..4] != b"XFSB" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ver = be16(&sb, 100) & 0xF;
|
||||||
|
let bs = be32(&sb, 4) as u64;
|
||||||
|
let agblocks = be32(&sb, 84) as u64;
|
||||||
|
let agcount = be32(&sb, 88) as u64;
|
||||||
|
let inodesize = be16(&sb, 104) as u64;
|
||||||
|
if !(512..=65536).contains(&bs)
|
||||||
|
|| !bs.is_power_of_two()
|
||||||
|
|| agblocks == 0
|
||||||
|
|| agcount == 0
|
||||||
|
|| !(256..=2048).contains(&inodesize)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let incompat = be32(&sb, 216);
|
||||||
|
let features2 = be32(&sb, 200);
|
||||||
|
let v5 = ver == 5;
|
||||||
|
Some(Xfs {
|
||||||
|
src,
|
||||||
|
base,
|
||||||
|
bs,
|
||||||
|
agblocks,
|
||||||
|
agcount,
|
||||||
|
inodesize,
|
||||||
|
inopblog: sb[123] as u32,
|
||||||
|
agblklog: sb[124] as u32,
|
||||||
|
sectsize: be16(&sb, 102) as u64,
|
||||||
|
v5,
|
||||||
|
ftype: if v5 {
|
||||||
|
incompat & 1 != 0
|
||||||
|
} else {
|
||||||
|
features2 & 0x200 != 0
|
||||||
|
},
|
||||||
|
root_ino: be64(&sb, 56),
|
||||||
|
dblocks: be64(&sb, 8),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ag_off(&self, agno: u64) -> u64 {
|
||||||
|
self.base + agno * self.agblocks * self.bs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endereço físico de um bloco "fsblock" (agno << agblklog | agbno).
|
||||||
|
fn fsb_off(&self, fsb: u64) -> Option<u64> {
|
||||||
|
let (agno, agbno) = (fsb >> self.agblklog, fsb & ((1 << self.agblklog) - 1));
|
||||||
|
(agno < self.agcount && agbno < self.agblocks).then(|| self.ag_off(agno) + agbno * self.bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ino_off(&self, ino: u64) -> Option<u64> {
|
||||||
|
let shift = self.agblklog + self.inopblog;
|
||||||
|
let agno = ino >> shift;
|
||||||
|
let agbno = (ino >> self.inopblog) & ((1 << self.agblklog) - 1);
|
||||||
|
let idx = ino & ((1 << self.inopblog) - 1);
|
||||||
|
(agno < self.agcount && agbno < self.agblocks)
|
||||||
|
.then(|| self.ag_off(agno) + agbno * self.bs + idx * self.inodesize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ino(&self, ino: u64) -> Option<Vec<u8>> {
|
||||||
|
let b = read_vec(self.src, self.ino_off(ino)?, self.inodesize as usize).ok()?;
|
||||||
|
(b.len() as u64 == self.inodesize && &b[0..2] == b"IN").then_some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn core_size(&self, ino: &[u8]) -> usize {
|
||||||
|
if ino[4] >= 3 {
|
||||||
|
176
|
||||||
|
} else {
|
||||||
|
100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extents de um inode com a lista no próprio inode (formato 2) ou em árvore (formato 3).
|
||||||
|
fn extents_of(&self, ino: &[u8], max_blocks: u64) -> Vec<(u64, u64, u64)> {
|
||||||
|
let fmt = ino[5];
|
||||||
|
let start = self.core_size(ino);
|
||||||
|
let fork_end = if ino[82] != 0 {
|
||||||
|
start.max(100 + ino[82] as usize * 8)
|
||||||
|
} else {
|
||||||
|
self.inodesize as usize
|
||||||
|
};
|
||||||
|
let lit = &ino[start..fork_end.min(ino.len())];
|
||||||
|
let mut out = vec![];
|
||||||
|
match fmt {
|
||||||
|
2 => {
|
||||||
|
let n = (be32(ino, 76) as usize).min(lit.len() / 16);
|
||||||
|
for i in 0..n {
|
||||||
|
out.push(unpack_extent(&lit[i * 16..i * 16 + 16]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
// raiz da árvore de extents no inode: nível, nregistros, chaves e ponteiros
|
||||||
|
let level = be16(lit, 0) as usize;
|
||||||
|
let nrecs = be16(lit, 2) as usize;
|
||||||
|
let maxrecs = (lit.len() - 4) / 16;
|
||||||
|
let ptrs = 4 + maxrecs * 8;
|
||||||
|
for i in 0..nrecs.min(maxrecs) {
|
||||||
|
let fsb = be64(lit, ptrs + i * 8);
|
||||||
|
self.walk_bmbt(fsb, level - 1, &mut out, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
out.retain(|e| e.2 > 0 && e.2 <= max_blocks);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_bmbt(&self, fsb: u64, level: usize, out: &mut Vec<(u64, u64, u64)>, depth: u32) {
|
||||||
|
if depth > 6 || out.len() > 1_000_000 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(off) = self.fsb_off(fsb) else { return };
|
||||||
|
let Ok(b) = read_vec(self.src, off, self.bs as usize) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if b.len() < 24 || !(&b[0..4] == b"BMAP" || &b[0..4] == b"BMA3") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let hdr = if self.v5 { 72 } else { 24 };
|
||||||
|
let numrecs = be16(&b, 6) as usize;
|
||||||
|
if level == 0 {
|
||||||
|
for i in 0..numrecs {
|
||||||
|
if let Some(r) = b.get(hdr + i * 16..hdr + i * 16 + 16) {
|
||||||
|
out.push(unpack_extent(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let maxrecs = (self.bs as usize - hdr) / 16;
|
||||||
|
let ptrs = hdr + maxrecs * 8;
|
||||||
|
for i in 0..numrecs {
|
||||||
|
self.walk_bmbt(be64(&b, ptrs + i * 8), level - 1, out, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registro de extent empacotado em 128 bits: flag(1) | início no arquivo(54) | bloco(52) | quantidade(21).
|
||||||
|
fn unpack_extent(r: &[u8]) -> (u64, u64, u64) {
|
||||||
|
let hi = u64::from_be_bytes(r[0..8].try_into().unwrap());
|
||||||
|
let lo = u64::from_be_bytes(r[8..16].try_into().unwrap());
|
||||||
|
let startoff = (hi >> 9) & ((1 << 54) - 1);
|
||||||
|
let startblock = ((hi & 0x1FF) << 43) | (lo >> 21);
|
||||||
|
let count = lo & 0x1F_FFFF;
|
||||||
|
(startoff, startblock, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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]),
|
||||||
|
) {
|
||||||
|
let mut cur = root;
|
||||||
|
// desce pela esquerda até a folha
|
||||||
|
for lvl in (1..levels).rev() {
|
||||||
|
let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if b.len() < 24 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let hdr = if x.v5 { 56 } else { 16 };
|
||||||
|
let maxrecs = (x.bs as usize - hdr) / (key_size + 4);
|
||||||
|
let ptrs = hdr + maxrecs * key_size;
|
||||||
|
let _ = lvl;
|
||||||
|
cur = be32(&b, ptrs) as u64;
|
||||||
|
}
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
while cur != 0 && cur != u32::MAX as u64 && seen.insert(cur) {
|
||||||
|
let Ok(b) = read_vec(x.src, x.ag_off(agno) + cur * x.bs, x.bs as usize) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if b.len() < 24 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let hdr = if x.v5 { 56 } else { 16 };
|
||||||
|
let n = be16(&b, 6) as usize;
|
||||||
|
for i in 0..n {
|
||||||
|
if let Some(r) = b.get(hdr + i * rec..hdr + (i + 1) * rec) {
|
||||||
|
f(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur = be32(&b, 12) as u64; // rightsib
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
||||||
|
let _ = part_len;
|
||||||
|
let mut res = VolumeScan::default();
|
||||||
|
let Some(x) = Xfs::open(ctx.src, base) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Espaço livre por grupo e inodes livres.
|
||||||
|
let mut free: Vec<Extent> = vec![];
|
||||||
|
let mut free_set: Vec<(u64, u64)> = vec![]; // (fsblock inicial, quantidade) para checar blocos
|
||||||
|
let mut chunks: Vec<(u64, u64, u64)> = vec![]; // (agno, startino, máscara livre)
|
||||||
|
for agno in 0..x.agcount {
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
let agf = read_vec(ctx.src, x.ag_off(agno) + x.sectsize, 64).unwrap_or_default();
|
||||||
|
if agf.len() >= 64 && &agf[0..4] == b"XAGF" {
|
||||||
|
let (root, levels) = (be32(&agf, 16) as u64, be32(&agf, 28));
|
||||||
|
walk_short_btree(&x, agno, root, levels, 8, 8, &mut |r| {
|
||||||
|
let (s, c) = (be32(r, 0) as u64, be32(r, 4) as u64);
|
||||||
|
free.push(Extent::new(x.ag_off(agno) + s * x.bs, c * x.bs));
|
||||||
|
free_set.push(((agno << x.agblklog) | s, c));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let agi = read_vec(ctx.src, x.ag_off(agno) + 2 * x.sectsize, 64).unwrap_or_default();
|
||||||
|
if agi.len() >= 64 && &agi[0..4] == b"XAGI" {
|
||||||
|
let (root, levels) = (be32(&agi, 20) as u64, be32(&agi, 24));
|
||||||
|
walk_short_btree(&x, agno, root, levels, 16, 4, &mut |r| {
|
||||||
|
chunks.push((agno, be32(r, 0) as u64, be64(r, 8)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
(ctx.progress)(agno, x.agcount);
|
||||||
|
}
|
||||||
|
free.sort_by_key(|e| e.offset);
|
||||||
|
let free = merge_extents(free);
|
||||||
|
res.free = Some(free.clone());
|
||||||
|
free_set.sort();
|
||||||
|
let is_free = |fsb: u64, count: u64| -> bool {
|
||||||
|
let i = free_set.partition_point(|&(s, c)| s + c <= fsb);
|
||||||
|
free_set
|
||||||
|
.get(i)
|
||||||
|
.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).
|
||||||
|
let (live_dirs, stale_names) = collect_names(&x, ctx);
|
||||||
|
|
||||||
|
// Inodes livres com a lista de extents ainda dentro do inode.
|
||||||
|
let max_blocks = x.dblocks;
|
||||||
|
let mut seen_first: HashSet<u64> = HashSet::new();
|
||||||
|
for (agno, startino, mask) in &chunks {
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
for i in 0..64u64 {
|
||||||
|
if mask >> i & 1 == 0 {
|
||||||
|
continue; // em uso
|
||||||
|
}
|
||||||
|
let ino = (agno << (x.agblklog + x.inopblog)) | (startino + i);
|
||||||
|
let Some(raw) = x.read_ino(ino) else { continue };
|
||||||
|
let mode = be16(&raw, 2);
|
||||||
|
if mode != 0 && be32(&raw, 16) != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Só inodes que foram arquivos: o formato de extents continua gravado e há sobras.
|
||||||
|
if raw[5] != 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let start = x.core_size(&raw);
|
||||||
|
let lit = &raw[start..];
|
||||||
|
// lê registros plausíveis em sequência
|
||||||
|
let mut ext: Vec<(u64, u64, u64)> = vec![];
|
||||||
|
let mut next_off = 0u64;
|
||||||
|
for k in 0..lit.len() / 16 {
|
||||||
|
let e = unpack_extent(&lit[k * 16..k * 16 + 16]);
|
||||||
|
let valid = e.2 > 0
|
||||||
|
&& e.2 < 1 << 21
|
||||||
|
&& e.0 == next_off
|
||||||
|
&& x.fsb_off(e.1).is_some()
|
||||||
|
&& (e.1 & ((1 << x.agblklog) - 1)) + e.2 <= x.agblocks;
|
||||||
|
if !valid {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
next_off = e.0 + e.2;
|
||||||
|
ext.push(e);
|
||||||
|
}
|
||||||
|
if ext.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let total_blocks: u64 = ext.iter().map(|e| e.2).sum();
|
||||||
|
if total_blocks > max_blocks {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
if extents.is_empty() || !seen_first.insert(extents[0].offset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut size = total_blocks * x.bs;
|
||||||
|
let mut conf = if all_free {
|
||||||
|
Confidence::High
|
||||||
|
} else {
|
||||||
|
Confidence::Low
|
||||||
|
};
|
||||||
|
let mut note = if all_free {
|
||||||
|
"inode XFS liberado; lista de extents recuperada do próprio inode".to_string()
|
||||||
|
} else {
|
||||||
|
"blocos do arquivo já reaproveitados".to_string()
|
||||||
|
};
|
||||||
|
// tamanho exato: tenta achar o fim real pelo conteúdo
|
||||||
|
let mut est = true;
|
||||||
|
if let Some(h) = read_vec(ctx.src, extents[0].offset, 128)
|
||||||
|
.ok()
|
||||||
|
.and_then(|head| signatures::detect(ctx.src, extents[0].offset, &head, size))
|
||||||
|
{
|
||||||
|
if h.len > 0 && h.len <= size {
|
||||||
|
size = h.len;
|
||||||
|
est = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if est {
|
||||||
|
conf = conf.min(Confidence::Medium);
|
||||||
|
note.push_str("; tamanho exato perdido: usando todos os blocos");
|
||||||
|
}
|
||||||
|
let extents = super::fat::trim_extents(extents, size);
|
||||||
|
let low = ino & 0xFFFF_FFFF;
|
||||||
|
let (dir, name) = match stale_names.get(&low) {
|
||||||
|
Some((parent, n)) => (
|
||||||
|
Some(live_dirs.get(parent).cloned().unwrap_or_default()),
|
||||||
|
n.clone(),
|
||||||
|
),
|
||||||
|
None => (None, format!("inode_{ino}")),
|
||||||
|
};
|
||||||
|
let ts = |s: u32| (s != 0).then_some(s as i64);
|
||||||
|
let nf = NewFound {
|
||||||
|
name,
|
||||||
|
dir,
|
||||||
|
size,
|
||||||
|
origin: Origin::Xfs,
|
||||||
|
data: DataLoc::Extents { extents },
|
||||||
|
modified: ts(be32(&raw, 40)),
|
||||||
|
created: if raw[4] >= 3 {
|
||||||
|
ts(be32(&raw, 144))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
accessed: ts(be32(&raw, 32)),
|
||||||
|
base: conf,
|
||||||
|
note: Some(¬e),
|
||||||
|
};
|
||||||
|
let f = make_found(ctx, nf);
|
||||||
|
(ctx.emit)(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Percorre os diretórios vivos; devolve (caminho de cada diretório por inode, nome liberado por inode baixo).
|
||||||
|
fn collect_names(x: &Xfs, ctx: &Ctx) -> (HashMap<u64, String>, HashMap<u64, (u64, String)>) {
|
||||||
|
let mut paths: HashMap<u64, String> = HashMap::new();
|
||||||
|
let mut stale: HashMap<u64, (u64, String)> = HashMap::new();
|
||||||
|
let mut queue: Vec<(u64, String)> = vec![(x.root_ino, String::new())];
|
||||||
|
let mut seen: HashSet<u64> = HashSet::new();
|
||||||
|
while let Some((dino, path)) = queue.pop() {
|
||||||
|
if !seen.insert(dino) || (ctx.cancelled)() || seen.len() > 200_000 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(raw) = x.read_ino(dino) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if be16(&raw, 2) & 0o170000 != 0o040000 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
paths.insert(dino, path.clone());
|
||||||
|
let start = x.core_size(&raw);
|
||||||
|
match raw[5] {
|
||||||
|
1 => {
|
||||||
|
// pasta curta guardada no inode
|
||||||
|
let lit = &raw[start..];
|
||||||
|
let count = lit[0] as usize;
|
||||||
|
let i8 = lit[1] != 0;
|
||||||
|
let isz = if i8 { 8 } else { 4 };
|
||||||
|
let mut p = 2 + isz;
|
||||||
|
for _ in 0..count.max(lit[1] as usize) {
|
||||||
|
if p + 3 > lit.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let nl = lit[p] as usize;
|
||||||
|
p += 3;
|
||||||
|
let name =
|
||||||
|
String::from_utf8_lossy(lit.get(p..p + nl).unwrap_or(&[])).to_string();
|
||||||
|
p += nl + if x.ftype { 1 } else { 0 };
|
||||||
|
let ino = if i8 {
|
||||||
|
be64(lit, p)
|
||||||
|
} else {
|
||||||
|
be32(lit, p) as u64
|
||||||
|
};
|
||||||
|
p += isz;
|
||||||
|
queue.push((ino, format!("{path}/{name}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 | 3 => {
|
||||||
|
let size = be64(&raw, 56).min(64 << 20);
|
||||||
|
let ex = x.extents_of(&raw, 1 << 20);
|
||||||
|
let _ = size;
|
||||||
|
for (off, fsb, cnt) in ex {
|
||||||
|
// blocos de dados de diretório ficam antes do offset de 32 GiB (depois vêm folhas e índice)
|
||||||
|
if off * x.bs >= 32u64 << 30 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for k in 0..cnt.min(256) {
|
||||||
|
let Some(o) = x.fsb_off(fsb + k) else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(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)>,
|
||||||
|
) {
|
||||||
|
let magic = &b[0..4];
|
||||||
|
let hdr = match magic {
|
||||||
|
b"XD2B" | b"XD2D" => 16,
|
||||||
|
b"XDB3" | b"XDD3" => 64,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let is_block = magic == b"XD2B" || magic == b"XDB3";
|
||||||
|
// em blocos únicos, o final tem a tabela de folhas
|
||||||
|
let end = if is_block {
|
||||||
|
let cnt = be32(b, b.len() - 8) as usize;
|
||||||
|
b.len().saturating_sub(8 + cnt * 8)
|
||||||
|
} else {
|
||||||
|
b.len()
|
||||||
|
};
|
||||||
|
let mut p = hdr;
|
||||||
|
while p + 8 <= end {
|
||||||
|
if be16(b, p) == 0xFFFF {
|
||||||
|
let len = be16(b, p + 2) as usize;
|
||||||
|
if len < 8 || p + len > end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// entrada liberada: sobraram (inode baixo, nome); outras ainda podem estar dentro da região
|
||||||
|
scan_free_region(x, &b[p..p + len], dino, stale);
|
||||||
|
p += len;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ino = be64(b, p);
|
||||||
|
let nl = b[p + 8] as usize;
|
||||||
|
if nl == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let ft = if x.ftype { 1 } else { 0 };
|
||||||
|
let size = (8 + 1 + nl + ft + 2).div_ceil(8) * 8;
|
||||||
|
if p + size > end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let name = String::from_utf8_lossy(&b[p + 9..p + 9 + nl]).to_string();
|
||||||
|
if name != "." && name != ".." {
|
||||||
|
queue.push((ino, format!("{path}/{name}")));
|
||||||
|
}
|
||||||
|
p += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_free_region(x: &Xfs, r: &[u8], dir_ino: u64, stale: &mut HashMap<u64, (u64, String)>) {
|
||||||
|
// 1) a primeira entrada perdeu só os 4 primeiros bytes do número do inode
|
||||||
|
let try_at = |p: usize, low: u64, stale: &mut HashMap<u64, (u64, String)>| -> Option<usize> {
|
||||||
|
let nl = *r.get(p + 8)? as usize;
|
||||||
|
let name = r.get(p + 9..p + 9 + nl)?;
|
||||||
|
if nl == 0 || name.iter().any(|&c| c < 0x20 || c == b'/' || c == 0x7F) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
if r.len() >= 16 {
|
||||||
|
let low = be32(r, 4) as u64;
|
||||||
|
if let Some(sz) = try_at(0, low, stale) {
|
||||||
|
// entradas seguintes dentro da mesma região, com o inode inteiro
|
||||||
|
let mut p = sz;
|
||||||
|
while p + 16 <= r.len() {
|
||||||
|
let ino = be64(r, p);
|
||||||
|
if ino == 0 || ino > 1 << 56 {
|
||||||
|
p += 8;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match try_at(p, ino & 0xFFFF_FFFF, stale) {
|
||||||
|
Some(s) => p += s,
|
||||||
|
None => p += 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,111 @@ pub struct ImageReport {
|
|||||||
const BLOCK: usize = 1 << 20;
|
const BLOCK: usize = 1 << 20;
|
||||||
const SECTOR: usize = 512;
|
const SECTOR: usize = 512;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||||
|
pub enum ImageFormat {
|
||||||
|
#[default]
|
||||||
|
Raw,
|
||||||
|
/// Expert Witness (EnCase 6): `.E01`, dividido em segmentos.
|
||||||
|
E01 {
|
||||||
|
#[serde(default)]
|
||||||
|
case_number: String,
|
||||||
|
#[serde(default)]
|
||||||
|
description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
evidence_number: String,
|
||||||
|
#[serde(default)]
|
||||||
|
examiner: String,
|
||||||
|
#[serde(default)]
|
||||||
|
notes: String,
|
||||||
|
/// Tamanho máximo de cada segmento em MiB (0 = um arquivo só).
|
||||||
|
#[serde(default = "default_segment_mib")]
|
||||||
|
segment_mib: u64,
|
||||||
|
/// 0 = sem compressão, 1 a 9 = deflate.
|
||||||
|
#[serde(default = "default_compression")]
|
||||||
|
compression: u32,
|
||||||
|
},
|
||||||
|
/// AFF4 (padrão v1.0, blocos snappy).
|
||||||
|
Aff4 {
|
||||||
|
#[serde(default)]
|
||||||
|
case_number: String,
|
||||||
|
#[serde(default)]
|
||||||
|
examiner: String,
|
||||||
|
#[serde(default)]
|
||||||
|
evidence_number: String,
|
||||||
|
#[serde(default)]
|
||||||
|
notes: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_segment_mib() -> u64 {
|
||||||
|
1500
|
||||||
|
}
|
||||||
|
fn default_compression() -> u32 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_image_as(
|
||||||
|
src: &dyn Source,
|
||||||
|
dest: &Path,
|
||||||
|
format: &ImageFormat,
|
||||||
|
cancelled: &(dyn Fn() -> bool + Sync),
|
||||||
|
progress: &mut dyn FnMut(u64, u64),
|
||||||
|
) -> Result<ImageReport> {
|
||||||
|
match format {
|
||||||
|
ImageFormat::Raw => create_image(src, dest, cancelled, progress),
|
||||||
|
ImageFormat::Aff4 {
|
||||||
|
case_number,
|
||||||
|
examiner,
|
||||||
|
evidence_number,
|
||||||
|
notes,
|
||||||
|
} => {
|
||||||
|
let opts = crate::aff4::Aff4Options {
|
||||||
|
case_name: case_number.clone(),
|
||||||
|
examiner: examiner.clone(),
|
||||||
|
notes: notes.clone(),
|
||||||
|
evidence_number: evidence_number.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let dest = if dest
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|e| e.eq_ignore_ascii_case("aff4"))
|
||||||
|
{
|
||||||
|
dest.to_path_buf()
|
||||||
|
} else {
|
||||||
|
dest.with_extension("aff4")
|
||||||
|
};
|
||||||
|
crate::aff4::write_aff4(src, &dest, &opts, cancelled, progress)
|
||||||
|
}
|
||||||
|
ImageFormat::E01 {
|
||||||
|
case_number,
|
||||||
|
description,
|
||||||
|
evidence_number,
|
||||||
|
examiner,
|
||||||
|
notes,
|
||||||
|
segment_mib,
|
||||||
|
compression,
|
||||||
|
} => {
|
||||||
|
let opts = crate::ewf::E01Options {
|
||||||
|
case_number: case_number.clone(),
|
||||||
|
description: description.clone(),
|
||||||
|
evidence_number: evidence_number.clone(),
|
||||||
|
examiner: examiner.clone(),
|
||||||
|
notes: notes.clone(),
|
||||||
|
segment_size: segment_mib * 1024 * 1024,
|
||||||
|
compression: *compression,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// O destino pode vir com ou sem a extensão .E01.
|
||||||
|
let base = match dest.extension().and_then(|e| e.to_str()) {
|
||||||
|
Some(e) if e.eq_ignore_ascii_case("e01") => dest.with_extension(""),
|
||||||
|
_ => dest.to_path_buf(),
|
||||||
|
};
|
||||||
|
crate::ewf::write_e01(src, &base, &opts, cancelled, progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_image(
|
pub fn create_image(
|
||||||
src: &dyn Source,
|
src: &dyn Source,
|
||||||
dest: &Path,
|
dest: &Path,
|
||||||
|
|||||||
@@ -0,0 +1,664 @@
|
|||||||
|
//! Decodificador de entropia de JPEG sequencial (Huffman, 8 bits), sem IDCT.
|
||||||
|
//!
|
||||||
|
//! Serve para validar se os bytes de um JPEG são realmente contínuos e para reconstruir arquivos
|
||||||
|
//! fragmentados: o fluxo é decodificado MCU a MCU, guardando o DC de luminância (uma miniatura
|
||||||
|
//! 1/8 da imagem) e pontos de retomada. Depois de uma quebra os bytes viram lixo, e a
|
||||||
|
//! descontinuidade do DC (ou um erro de código) denuncia onde.
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Huff {
|
||||||
|
// decodificação canônica
|
||||||
|
mincode: [i32; 17],
|
||||||
|
maxcode: [i32; 18],
|
||||||
|
valptr: [i32; 17],
|
||||||
|
vals: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl 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 code = 0i32;
|
||||||
|
let mut k = 0i32;
|
||||||
|
for l in 1..=16 {
|
||||||
|
h.valptr[l] = k;
|
||||||
|
h.mincode[l] = code;
|
||||||
|
code += counts[l - 1] as i32;
|
||||||
|
k += counts[l - 1] as i32;
|
||||||
|
h.maxcode[l] = if counts[l - 1] > 0 { code - 1 } else { -1 };
|
||||||
|
code <<= 1;
|
||||||
|
}
|
||||||
|
h.maxcode[17] = i32::MAX;
|
||||||
|
h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Comp {
|
||||||
|
pub h: u8,
|
||||||
|
pub v: u8,
|
||||||
|
pub tq: usize,
|
||||||
|
pub dc_tbl: usize,
|
||||||
|
pub ac_tbl: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Header {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub comps: Vec<Comp>,
|
||||||
|
pub restart: u32,
|
||||||
|
pub mcus_x: u32,
|
||||||
|
pub mcus_y: u32,
|
||||||
|
/// Onde começam os dados entropy-coded (depois do SOS).
|
||||||
|
pub scan_start: usize,
|
||||||
|
/// Tabelas de quantização (ordem zigzag).
|
||||||
|
pub qt: [[u16; 64]; 4],
|
||||||
|
dc: [Option<Huff>; 4],
|
||||||
|
ac: [Option<Huff>; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
pub fn total_mcus(&self) -> u32 {
|
||||||
|
self.mcus_x * self.mcus_y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lê os segmentos até o SOS. `None` se não for JPEG sequencial suportado.
|
||||||
|
pub fn parse_header(d: &[u8]) -> Option<Header> {
|
||||||
|
if d.len() < 4 || d[0] != 0xFF || d[1] != 0xD8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut p = 2;
|
||||||
|
let mut dc: [Option<Huff>; 4] = Default::default();
|
||||||
|
let mut ac: [Option<Huff>; 4] = Default::default();
|
||||||
|
let mut sof: Option<(u32, u32, Vec<(u8, u8, usize)>)> = None;
|
||||||
|
let mut qt = [[1u16; 64]; 4];
|
||||||
|
let mut restart = 0u32;
|
||||||
|
while p + 4 <= d.len() {
|
||||||
|
if d[p] != 0xFF {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let m = d[p + 1];
|
||||||
|
if m == 0xFF {
|
||||||
|
p += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (0xD0..=0xD8).contains(&m) || m == 0x01 {
|
||||||
|
p += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let len = u16::from_be_bytes([d[p + 2], d[p + 3]]) as usize;
|
||||||
|
if len < 2 || p + 2 + len > d.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let seg = &d[p + 4..p + 2 + len];
|
||||||
|
match m {
|
||||||
|
0xC0 | 0xC1 => {
|
||||||
|
if seg.len() < 6 || seg[0] != 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let h = u16::from_be_bytes([seg[1], seg[2]]) as u32;
|
||||||
|
let w = u16::from_be_bytes([seg[3], seg[4]]) as u32;
|
||||||
|
let n = seg[5] as usize;
|
||||||
|
if n == 0 || n > 4 || seg.len() < 6 + n * 3 || w == 0 || h == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let comps = (0..n)
|
||||||
|
.map(|i| {
|
||||||
|
(
|
||||||
|
seg[7 + i * 3] >> 4,
|
||||||
|
seg[7 + i * 3] & 15,
|
||||||
|
seg[8 + i * 3] as usize & 3,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
sof = Some((w, h, comps));
|
||||||
|
}
|
||||||
|
0xC2 | 0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => return None, // progressivo, aritmético etc.
|
||||||
|
0xC4 => {
|
||||||
|
let mut q = 0;
|
||||||
|
while q + 17 <= seg.len() {
|
||||||
|
let (cls, id) = (seg[q] >> 4, (seg[q] & 15) as usize);
|
||||||
|
let mut counts = [0u8; 16];
|
||||||
|
counts.copy_from_slice(&seg[q + 1..q + 17]);
|
||||||
|
let n: usize = counts.iter().map(|&c| c as usize).sum();
|
||||||
|
if id > 3 || q + 17 + n > seg.len() || n > 256 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let h = Huff::new(&counts, seg[q + 17..q + 17 + n].to_vec());
|
||||||
|
if cls == 0 {
|
||||||
|
dc[id] = Some(h);
|
||||||
|
} else {
|
||||||
|
ac[id] = Some(h);
|
||||||
|
}
|
||||||
|
q += 17 + n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xDB => {
|
||||||
|
let mut q = 0;
|
||||||
|
while q < seg.len() {
|
||||||
|
let (prec, id) = (seg[q] >> 4, (seg[q] & 15) as usize);
|
||||||
|
q += 1;
|
||||||
|
let n = if prec == 0 { 64 } else { 128 };
|
||||||
|
if id > 3 || q + n > seg.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for i in 0..64 {
|
||||||
|
qt[id][i] = if prec == 0 {
|
||||||
|
seg[q + i] as u16
|
||||||
|
} else {
|
||||||
|
u16::from_be_bytes([seg[q + 2 * i], seg[q + 2 * i + 1]])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
q += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xDD => {
|
||||||
|
if seg.len() >= 2 {
|
||||||
|
restart = u16::from_be_bytes([seg[0], seg[1]]) as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xDA => {
|
||||||
|
let (w, h, comps) = sof?;
|
||||||
|
let ns = *seg.first()? as usize;
|
||||||
|
if ns != comps.len() || seg.len() < 1 + ns * 2 + 3 {
|
||||||
|
return None; // varredura não intercalada
|
||||||
|
}
|
||||||
|
let mut cs = vec![];
|
||||||
|
for i in 0..ns {
|
||||||
|
let t = seg[2 + i * 2];
|
||||||
|
cs.push(Comp {
|
||||||
|
h: comps[i].0,
|
||||||
|
v: comps[i].1,
|
||||||
|
tq: comps[i].2,
|
||||||
|
dc_tbl: (t >> 4) as usize & 3,
|
||||||
|
ac_tbl: (t & 15) as usize & 3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let hmax = cs.iter().map(|c| c.h).max()? as u32;
|
||||||
|
let vmax = cs.iter().map(|c| c.v).max()? as u32;
|
||||||
|
if hmax == 0 || vmax == 0 || hmax > 4 || vmax > 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for c in &cs {
|
||||||
|
if c.h == 0 || c.v == 0 || dc[c.dc_tbl].is_none() || ac[c.ac_tbl].is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(Header {
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
comps: cs,
|
||||||
|
restart,
|
||||||
|
mcus_x: w.div_ceil(8 * hmax),
|
||||||
|
mcus_y: h.div_ceil(8 * vmax),
|
||||||
|
scan_start: p + 2 + len,
|
||||||
|
qt,
|
||||||
|
dc,
|
||||||
|
ac,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
p += 2 + len;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Stop {
|
||||||
|
/// Todos os MCUs foram decodificados.
|
||||||
|
Done,
|
||||||
|
/// Marcador inesperado (inclusive EOI) na posição dada.
|
||||||
|
Marker(u8, usize),
|
||||||
|
/// Código inválido ou coeficiente fora do limite.
|
||||||
|
Error(usize),
|
||||||
|
/// Faltam bytes.
|
||||||
|
Truncated,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estado completo do decodificador num limite de byte, para retomar com outros dados.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct State {
|
||||||
|
/// Próximo byte a buscar no fluxo.
|
||||||
|
pub pos: usize,
|
||||||
|
bitbuf: u32,
|
||||||
|
bitcnt: u32,
|
||||||
|
pred: [i32; 4],
|
||||||
|
pub mcu: u32,
|
||||||
|
rst_next: u8,
|
||||||
|
mcus_since_rst: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
/// Último DC de luminância (preditor) no ponto de retomada.
|
||||||
|
pub fn luma_pred(&self) -> i32 {
|
||||||
|
self.pred[0]
|
||||||
|
}
|
||||||
|
pub fn start(h: &Header) -> State {
|
||||||
|
State {
|
||||||
|
pos: h.scan_start,
|
||||||
|
bitbuf: 0,
|
||||||
|
bitcnt: 0,
|
||||||
|
pred: [0; 4],
|
||||||
|
mcu: 0,
|
||||||
|
rst_next: 0,
|
||||||
|
mcus_since_rst: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Decoded {
|
||||||
|
/// DC de luminância (absoluto) por MCU decodificado.
|
||||||
|
pub dc: Vec<i32>,
|
||||||
|
/// Estados guardados em intervalos (para retomada): (MCU, estado ao fim dele).
|
||||||
|
pub snaps: Vec<State>,
|
||||||
|
pub stop: Stop,
|
||||||
|
pub state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Bits<'a> {
|
||||||
|
d: &'a [u8],
|
||||||
|
st: State,
|
||||||
|
marker: Option<(u8, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bits<'_> {
|
||||||
|
fn fill(&mut self) {
|
||||||
|
while self.st.bitcnt <= 24 && self.marker.is_none() {
|
||||||
|
let Some(&b) = self.d.get(self.st.pos) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if b == 0xFF {
|
||||||
|
match self.d.get(self.st.pos + 1) {
|
||||||
|
Some(0) => {
|
||||||
|
self.st.bitbuf |= 0xFF << (24 - self.st.bitcnt);
|
||||||
|
self.st.bitcnt += 8;
|
||||||
|
self.st.pos += 2;
|
||||||
|
}
|
||||||
|
Some(&m) => {
|
||||||
|
self.marker = Some((m, self.st.pos));
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.st.bitbuf |= (b as u32) << (24 - self.st.bitcnt);
|
||||||
|
self.st.bitcnt += 8;
|
||||||
|
self.st.pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get(&mut self, n: u32) -> Option<u32> {
|
||||||
|
if n == 0 {
|
||||||
|
return Some(0);
|
||||||
|
}
|
||||||
|
if self.st.bitcnt < n {
|
||||||
|
self.fill();
|
||||||
|
if self.st.bitcnt < n {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let v = self.st.bitbuf >> (32 - n);
|
||||||
|
self.st.bitbuf <<= n;
|
||||||
|
self.st.bitcnt -= n;
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
fn huff(&mut self, h: &Huff) -> Option<u8> {
|
||||||
|
self.fill();
|
||||||
|
let mut code = 0i32;
|
||||||
|
for l in 1..=16u32 {
|
||||||
|
if self.st.bitcnt < l {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
code = (self.st.bitbuf >> (32 - l)) as i32;
|
||||||
|
if h.maxcode[l as usize] >= 0
|
||||||
|
&& code <= h.maxcode[l as usize]
|
||||||
|
&& code >= h.mincode[l as usize]
|
||||||
|
{
|
||||||
|
self.st.bitbuf <<= l;
|
||||||
|
self.st.bitcnt -= l;
|
||||||
|
let idx = h.valptr[l as usize] + code - h.mincode[l as usize];
|
||||||
|
return h.vals.get(idx as usize).copied();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = code;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extend(v: u32, t: u32) -> i32 {
|
||||||
|
if t == 0 {
|
||||||
|
0
|
||||||
|
} else if v < (1 << (t - 1)) {
|
||||||
|
v as i32 - (1 << t) + 1
|
||||||
|
} else {
|
||||||
|
v as i32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodifica até `max_mcus` MCUs a partir de `state`, sobre `data` (fluxo bruto com FF00).
|
||||||
|
/// `snap_every` > 0 guarda estados a cada tantos bytes consumidos.
|
||||||
|
pub fn decode(h: &Header, data: &[u8], state: State, max_mcus: u32, snap_every: usize) -> Decoded {
|
||||||
|
decode_hook(h, data, state, max_mcus, snap_every, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Como `decode`, mas entrega os coeficientes (zigzag) dos blocos de luminância de cada MCU.
|
||||||
|
pub fn decode_hook(
|
||||||
|
h: &Header,
|
||||||
|
data: &[u8],
|
||||||
|
state: State,
|
||||||
|
max_mcus: u32,
|
||||||
|
snap_every: usize,
|
||||||
|
mut hook: Option<&mut dyn FnMut(u32, &[[i32; 64]])>,
|
||||||
|
) -> Decoded {
|
||||||
|
let mut lblocks: Vec<[i32; 64]> = Vec::new();
|
||||||
|
let mut b = Bits {
|
||||||
|
d: data,
|
||||||
|
st: state,
|
||||||
|
marker: None,
|
||||||
|
};
|
||||||
|
let total = h.total_mcus();
|
||||||
|
let mut dc_out = Vec::new();
|
||||||
|
let mut snaps = Vec::new();
|
||||||
|
let mut last_snap = b.st.pos;
|
||||||
|
let mut stop = Stop::Done;
|
||||||
|
let mut decoded = 0u32;
|
||||||
|
'mcu: while b.st.mcu < total && decoded < max_mcus {
|
||||||
|
// Marcador de reinício entre MCUs.
|
||||||
|
if h.restart > 0 && b.st.mcus_since_rst == h.restart {
|
||||||
|
// descarta bits de preenchimento e espera RSTn
|
||||||
|
b.st.bitbuf = 0;
|
||||||
|
b.st.bitcnt = 0;
|
||||||
|
let p = b.st.pos;
|
||||||
|
match (data.get(p), data.get(p + 1)) {
|
||||||
|
(Some(0xFF), Some(&m)) if m == 0xD0 + b.st.rst_next => {
|
||||||
|
b.st.pos += 2;
|
||||||
|
b.st.rst_next = (b.st.rst_next + 1) & 7;
|
||||||
|
b.st.mcus_since_rst = 0;
|
||||||
|
b.st.pred = [0; 4];
|
||||||
|
b.marker = None;
|
||||||
|
}
|
||||||
|
(Some(0xFF), Some(&m)) => {
|
||||||
|
stop = Stop::Marker(m, p);
|
||||||
|
break 'mcu;
|
||||||
|
}
|
||||||
|
(None, _) | (_, None) => {
|
||||||
|
stop = Stop::Truncated;
|
||||||
|
break 'mcu;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
stop = Stop::Error(p);
|
||||||
|
break 'mcu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut luma_dc = 0;
|
||||||
|
lblocks.clear();
|
||||||
|
for (ci, c) in h.comps.iter().enumerate() {
|
||||||
|
let dct = h.dc[c.dc_tbl].as_ref().unwrap();
|
||||||
|
let act = h.ac[c.ac_tbl].as_ref().unwrap();
|
||||||
|
for blk in 0..(c.h as u32 * c.v as u32) {
|
||||||
|
let Some(t) = b.huff(dct) else {
|
||||||
|
stop = fail(&b, data);
|
||||||
|
break 'mcu;
|
||||||
|
};
|
||||||
|
if t > 11 {
|
||||||
|
stop = Stop::Error(b.st.pos);
|
||||||
|
break 'mcu;
|
||||||
|
}
|
||||||
|
let Some(bits) = b.get(t as u32) else {
|
||||||
|
stop = fail(&b, data);
|
||||||
|
break 'mcu;
|
||||||
|
};
|
||||||
|
b.st.pred[ci] += extend(bits, t as u32);
|
||||||
|
if ci == 0 && blk == 0 {
|
||||||
|
luma_dc = b.st.pred[0];
|
||||||
|
}
|
||||||
|
let mut coef = [0i32; 64];
|
||||||
|
if ci == 0 {
|
||||||
|
coef[0] = b.st.pred[0];
|
||||||
|
}
|
||||||
|
let mut k = 1;
|
||||||
|
while k < 64 {
|
||||||
|
let Some(rs) = b.huff(act) else {
|
||||||
|
stop = fail(&b, data);
|
||||||
|
break 'mcu;
|
||||||
|
};
|
||||||
|
let (r, s) = ((rs >> 4) as usize, (rs & 15) as u32);
|
||||||
|
if s == 0 {
|
||||||
|
if r == 15 {
|
||||||
|
k += 16;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
k += r;
|
||||||
|
if k > 63 {
|
||||||
|
stop = Stop::Error(b.st.pos);
|
||||||
|
break 'mcu;
|
||||||
|
}
|
||||||
|
let Some(vbits) = b.get(s) else {
|
||||||
|
stop = fail(&b, data);
|
||||||
|
break 'mcu;
|
||||||
|
};
|
||||||
|
if ci == 0 && hook.is_some() {
|
||||||
|
coef[k] = extend(vbits, s);
|
||||||
|
}
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
if ci == 0 && hook.is_some() {
|
||||||
|
lblocks.push(coef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(hk) = hook.as_mut() {
|
||||||
|
hk(b.st.mcu, &lblocks);
|
||||||
|
}
|
||||||
|
dc_out.push(luma_dc);
|
||||||
|
b.st.mcu += 1;
|
||||||
|
b.st.mcus_since_rst += 1;
|
||||||
|
decoded += 1;
|
||||||
|
if snap_every > 0 && b.st.pos >= last_snap + snap_every {
|
||||||
|
snaps.push(b.st.clone());
|
||||||
|
last_snap = b.st.pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Decoded {
|
||||||
|
dc: dc_out,
|
||||||
|
snaps,
|
||||||
|
stop,
|
||||||
|
state: b.st,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fail(b: &Bits, data: &[u8]) -> Stop {
|
||||||
|
if let Some((m, p)) = b.marker {
|
||||||
|
Stop::Marker(m, p)
|
||||||
|
} else if b.st.pos >= data.len() {
|
||||||
|
Stop::Truncated
|
||||||
|
} else {
|
||||||
|
Stop::Error(b.st.pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Primeiro MCU suspeito: janela de `win` MCUs cuja diferença média de DC é anormalmente alta.
|
||||||
|
/// Devolve o índice do MCU onde a janela começa.
|
||||||
|
pub fn first_discontinuity(h: &Header, dc: &[i32]) -> Option<usize> {
|
||||||
|
let mx = h.mcus_x as usize;
|
||||||
|
let d: Vec<f64> = (0..dc.len())
|
||||||
|
.map(|i| {
|
||||||
|
if i % mx > 0 {
|
||||||
|
(dc[i] - dc[i - 1]).abs() as f64
|
||||||
|
} else if i >= mx {
|
||||||
|
(dc[i] - dc[i - mx]).abs() as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let win = 24usize;
|
||||||
|
if d.len() < win * 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Referência: mediana das diferenças vistas até aqui (histograma, robusta a bordas).
|
||||||
|
let mut hist = vec![0u32; 2048];
|
||||||
|
let mut seen = 0u32;
|
||||||
|
let mut sum: f64 = d[..win].iter().sum();
|
||||||
|
for x in &d[..win] {
|
||||||
|
hist[(*x as usize).min(2047)] += 1;
|
||||||
|
seen += 1;
|
||||||
|
}
|
||||||
|
for i in win..d.len() {
|
||||||
|
sum += d[i] - d[i - win];
|
||||||
|
if i >= 64 + win && i % 8 == 0 {
|
||||||
|
let mut acc = 0u32;
|
||||||
|
let mut med = 0usize;
|
||||||
|
for (v, &c) in hist.iter().enumerate() {
|
||||||
|
acc += c;
|
||||||
|
if acc * 2 >= seen {
|
||||||
|
med = v;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sum / win as f64 > (6.0 * med as f64).max(40.0) {
|
||||||
|
return Some(i + 1 - win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Só entra na referência o que ainda parece normal.
|
||||||
|
if i < win * 3 || d[i] < 300.0 {
|
||||||
|
hist[(d[i] as usize).min(2047)] += 1;
|
||||||
|
seen += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------- pixels
|
||||||
|
|
||||||
|
const ZIGZAG: [usize; 64] = [
|
||||||
|
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20,
|
||||||
|
13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59,
|
||||||
|
52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
|
||||||
|
];
|
||||||
|
|
||||||
|
fn cos_table() -> &'static [[f32; 8]; 8] {
|
||||||
|
static T: std::sync::OnceLock<[[f32; 8]; 8]> = std::sync::OnceLock::new();
|
||||||
|
T.get_or_init(|| {
|
||||||
|
let mut t = [[0f32; 8]; 8];
|
||||||
|
for x in 0..8 {
|
||||||
|
for u in 0..8 {
|
||||||
|
let cu = if u == 0 {
|
||||||
|
std::f32::consts::FRAC_1_SQRT_2
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
t[x][u] = 0.5
|
||||||
|
* cu
|
||||||
|
* (((2 * x + 1) as f32 * u as f32 * std::f32::consts::PI) / 16.0).cos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IDCT de um bloco (coeficientes em zigzag, quantizados) para 8x8 pixels de luminância.
|
||||||
|
pub fn idct_block(coef: &[i32; 64], qt: &[u16; 64]) -> [u8; 64] {
|
||||||
|
let mut f = [[0f32; 8]; 8];
|
||||||
|
for z in 0..64 {
|
||||||
|
let n = ZIGZAG[z];
|
||||||
|
f[n / 8][n % 8] = (coef[z] * qt[z] as i32) as f32;
|
||||||
|
}
|
||||||
|
let t = cos_table();
|
||||||
|
// linhas
|
||||||
|
let mut tmp = [[0f32; 8]; 8];
|
||||||
|
for v in 0..8 {
|
||||||
|
for x in 0..8 {
|
||||||
|
let mut s = 0.0;
|
||||||
|
for u in 0..8 {
|
||||||
|
s += t[x][u] * f[v][u];
|
||||||
|
}
|
||||||
|
tmp[v][x] = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut out = [0u8; 64];
|
||||||
|
for y in 0..8 {
|
||||||
|
for x in 0..8 {
|
||||||
|
let mut s = 0.0;
|
||||||
|
for v in 0..8 {
|
||||||
|
s += t[y][v] * tmp[v][x];
|
||||||
|
}
|
||||||
|
out[y * 8 + x] = (s + 128.0).round().clamp(0.0, 255.0) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Luminância de um MCU (blocos `h` x `v` da primeira componente) como imagem (8h x 8v).
|
||||||
|
pub fn mcu_luma(h: &Header, blocks: &[[i32; 64]]) -> Vec<u8> {
|
||||||
|
let c = &h.comps[0];
|
||||||
|
let (bw, bh) = (c.h as usize, c.v as usize);
|
||||||
|
let w = bw * 8;
|
||||||
|
let mut out = vec![0u8; w * bh * 8];
|
||||||
|
for (i, blk) in blocks.iter().enumerate().take(bw * bh) {
|
||||||
|
let px = idct_block(blk, &h.qt[c.tq]);
|
||||||
|
let (bx, by) = (i % bw, i / bw);
|
||||||
|
for y in 0..8 {
|
||||||
|
out[(by * 8 + y) * w + bx * 8..(by * 8 + y) * w + bx * 8 + 8]
|
||||||
|
.copy_from_slice(&px[y * 8..y * 8 + 8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plano de luminância do que já foi decodificado.
|
||||||
|
pub struct Plane {
|
||||||
|
pub w: usize,
|
||||||
|
pub h: usize,
|
||||||
|
pub px: Vec<u8>,
|
||||||
|
pub mw: usize,
|
||||||
|
pub mh: usize,
|
||||||
|
pub have: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plane {
|
||||||
|
pub fn new(h: &Header) -> Plane {
|
||||||
|
let c = &h.comps[0];
|
||||||
|
let (mw, mh) = (c.h as usize * 8, c.v as usize * 8);
|
||||||
|
Plane {
|
||||||
|
w: mw * h.mcus_x as usize,
|
||||||
|
h: mh * h.mcus_y as usize,
|
||||||
|
px: vec![0; mw * h.mcus_x as usize * mh * h.mcus_y as usize],
|
||||||
|
mw,
|
||||||
|
mh,
|
||||||
|
have: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
for y in 0..self.mh {
|
||||||
|
let dst = (my * self.mh + y) * self.w + mx * self.mw;
|
||||||
|
self.px[dst..dst + self.mw].copy_from_slice(&luma[y * self.mw..(y + 1) * self.mw]);
|
||||||
|
}
|
||||||
|
self.have = self.have.max(mcu + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
let mut hook = |mcu: u32, blocks: &[[i32; 64]]| {
|
||||||
|
let l = mcu_luma(h, blocks);
|
||||||
|
plane.put(h, mcu, &l);
|
||||||
|
};
|
||||||
|
decode_hook(h, data, state, max_mcus, 512, Some(&mut hook))
|
||||||
|
}
|
||||||
@@ -1,22 +1,40 @@
|
|||||||
|
#![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
|
||||||
//! crate escreve no dispositivo que está sendo recuperado.
|
//! crate escreve no dispositivo que está sendo recuperado.
|
||||||
|
|
||||||
|
pub mod aff4;
|
||||||
|
pub mod assemble;
|
||||||
pub mod carve;
|
pub mod carve;
|
||||||
|
pub mod crypt;
|
||||||
|
pub mod defrag;
|
||||||
pub mod device;
|
pub mod device;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod ewf;
|
||||||
pub mod explorer;
|
pub mod explorer;
|
||||||
pub mod external;
|
pub mod external;
|
||||||
|
pub mod formats;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
pub mod hashing;
|
pub mod hashing;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
|
pub mod jpegscan;
|
||||||
|
pub mod lvm;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
|
pub mod mp4rebuild;
|
||||||
pub mod notify;
|
pub mod notify;
|
||||||
pub mod owner;
|
pub mod owner;
|
||||||
pub mod partition;
|
pub mod partition;
|
||||||
|
pub mod raid;
|
||||||
|
pub mod raid_detect;
|
||||||
pub mod reconnect;
|
pub mod reconnect;
|
||||||
pub mod recover;
|
pub mod recover;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
@@ -29,6 +47,7 @@ pub mod source;
|
|||||||
pub mod sqlite;
|
pub mod sqlite;
|
||||||
pub mod thumb;
|
pub mod thumb;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
pub mod volume;
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|||||||
@@ -0,0 +1,612 @@
|
|||||||
|
//! LVM2: lê o rótulo do volume físico (PV) e o texto de metadados, e remonta os volumes
|
||||||
|
//! lógicos (lineares, em faixas, espelhados e RAID 0/1/5/6/10 do LVM).
|
||||||
|
|
||||||
|
use crate::raid::{Layout, Level, Member, RaidParams, RaidSource};
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
const MDA_MAGIC: &[u8; 16] = b" LVM2 x[5A%r0N*>";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PvInfo {
|
||||||
|
/// UUID de 32 caracteres, sem hífens.
|
||||||
|
pub uuid: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub seqno: u64,
|
||||||
|
pub vg_name: String,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lê rótulo + metadados de um PV.
|
||||||
|
pub fn read_pv(src: &dyn Source) -> Option<PvInfo> {
|
||||||
|
let head = read_vec(src, 0, 2048).ok()?;
|
||||||
|
if head.len() < 2048 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let sector = (0..4).find(|&s| &head[s * 512..s * 512 + 8] == b"LABELONE")?;
|
||||||
|
let lab = &head[sector * 512..(sector + 1) * 512];
|
||||||
|
if &lab[24..32] != b"LVM2 001" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let off = u32::from_le_bytes(lab[20..24].try_into().ok()?) as usize;
|
||||||
|
let pv = lab.get(off..)?;
|
||||||
|
let uuid = String::from_utf8_lossy(pv.get(..32)?).to_string();
|
||||||
|
let size = u64::from_le_bytes(pv.get(32..40)?.try_into().ok()?);
|
||||||
|
// Lista de áreas de dados, depois de áreas de metadados; cada lista termina em (0,0).
|
||||||
|
let mut o = 40;
|
||||||
|
let rd = |o: usize| -> Option<(u64, u64)> {
|
||||||
|
Some((
|
||||||
|
u64::from_le_bytes(pv.get(o..o + 8)?.try_into().ok()?),
|
||||||
|
u64::from_le_bytes(pv.get(o + 8..o + 16)?.try_into().ok()?),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let (a, b) = rd(o)?;
|
||||||
|
o += 16;
|
||||||
|
if a == 0 && b == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut mdas = vec![];
|
||||||
|
loop {
|
||||||
|
let (a, b) = rd(o)?;
|
||||||
|
o += 16;
|
||||||
|
if a == 0 && b == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mdas.push((a, b));
|
||||||
|
}
|
||||||
|
let mut best: Option<PvInfo> = None;
|
||||||
|
for (mda_off, mda_size) in mdas {
|
||||||
|
let Ok(h) = read_vec(src, mda_off, 512) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if h.len() < 512 || &h[4..20] != MDA_MAGIC {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// raw_locn[0]
|
||||||
|
let loc_off = u64::from_le_bytes(h[40..48].try_into().unwrap());
|
||||||
|
let loc_size = u64::from_le_bytes(h[48..56].try_into().unwrap());
|
||||||
|
if loc_size == 0 || loc_size > 16 << 20 || mda_size < 1024 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut text = vec![];
|
||||||
|
if loc_off + loc_size <= mda_size {
|
||||||
|
if let Ok(t) = read_vec(src, mda_off + loc_off, loc_size as usize) {
|
||||||
|
text = t;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// O texto dá a volta no anel.
|
||||||
|
let first = mda_size - loc_off;
|
||||||
|
if let (Ok(a), Ok(b)) = (
|
||||||
|
read_vec(src, mda_off + loc_off, first as usize),
|
||||||
|
read_vec(src, mda_off + 512, (loc_size - first) as usize),
|
||||||
|
) {
|
||||||
|
text = [a, b].concat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let s = String::from_utf8_lossy(&text)
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.to_string();
|
||||||
|
let Some(tree) = parse(&s) else { continue };
|
||||||
|
let Some((vg_name, vg)) = tree
|
||||||
|
.sections
|
||||||
|
.iter()
|
||||||
|
.find(|(_, sec)| sec.values.contains_key("extent_size"))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let seqno = vg.int("seqno").unwrap_or(0);
|
||||||
|
let cand = PvInfo {
|
||||||
|
uuid: uuid.clone(),
|
||||||
|
size,
|
||||||
|
seqno,
|
||||||
|
vg_name: vg_name.clone(),
|
||||||
|
text: s.clone(),
|
||||||
|
};
|
||||||
|
if best.as_ref().map(|b| cand.seqno > b.seqno).unwrap_or(true) {
|
||||||
|
best = Some(cand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ texto
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Val {
|
||||||
|
Int(i64),
|
||||||
|
Str(String),
|
||||||
|
List(Vec<Val>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Section {
|
||||||
|
pub values: BTreeMap<String, Val>,
|
||||||
|
/// Subseções na ordem do arquivo.
|
||||||
|
pub sections: Vec<(String, Section)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Section {
|
||||||
|
pub fn int(&self, k: &str) -> Option<u64> {
|
||||||
|
match self.values.get(k)? {
|
||||||
|
Val::Int(i) => Some(*i as u64),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn str(&self, k: &str) -> Option<&str> {
|
||||||
|
match self.values.get(k)? {
|
||||||
|
Val::Str(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn list(&self, k: &str) -> Option<&[Val]> {
|
||||||
|
match self.values.get(k)? {
|
||||||
|
Val::List(l) => Some(l),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn sub(&self, name: &str) -> Option<&Section> {
|
||||||
|
self.sections
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _)| n == name)
|
||||||
|
.map(|(_, s)| s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Lexer<'a> {
|
||||||
|
b: &'a [u8],
|
||||||
|
i: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lexer<'_> {
|
||||||
|
fn skip(&mut self) {
|
||||||
|
loop {
|
||||||
|
while self.i < self.b.len()
|
||||||
|
&& (self.b[self.i].is_ascii_whitespace() || self.b[self.i] == b',')
|
||||||
|
{
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
if self.i < self.b.len() && self.b[self.i] == b'#' {
|
||||||
|
while self.i < self.b.len() && self.b[self.i] != b'\n' {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn ident(&mut self) -> Option<String> {
|
||||||
|
self.skip();
|
||||||
|
let s = self.i;
|
||||||
|
while self.i < self.b.len()
|
||||||
|
&& !self.b[self.i].is_ascii_whitespace()
|
||||||
|
&& !matches!(
|
||||||
|
self.b[self.i],
|
||||||
|
b'=' | b'{' | b'}' | b'[' | b']' | b'"' | b'#'
|
||||||
|
)
|
||||||
|
{
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
(self.i > s).then(|| String::from_utf8_lossy(&self.b[s..self.i]).to_string())
|
||||||
|
}
|
||||||
|
fn value(&mut self) -> Option<Val> {
|
||||||
|
self.skip();
|
||||||
|
match *self.b.get(self.i)? {
|
||||||
|
b'"' => {
|
||||||
|
self.i += 1;
|
||||||
|
let s = self.i;
|
||||||
|
while self.i < self.b.len() && self.b[self.i] != b'"' {
|
||||||
|
if self.b[self.i] == b'\\' {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
let v = String::from_utf8_lossy(&self.b[s..self.i.min(self.b.len())]).to_string();
|
||||||
|
self.i += 1;
|
||||||
|
Some(Val::Str(v))
|
||||||
|
}
|
||||||
|
b'[' => {
|
||||||
|
self.i += 1;
|
||||||
|
let mut l = vec![];
|
||||||
|
loop {
|
||||||
|
self.skip();
|
||||||
|
if *self.b.get(self.i)? == b']' {
|
||||||
|
self.i += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
l.push(self.value()?);
|
||||||
|
}
|
||||||
|
Some(Val::List(l))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let s = self.i;
|
||||||
|
while self.i < self.b.len()
|
||||||
|
&& (self.b[self.i].is_ascii_digit() || self.b[self.i] == b'-')
|
||||||
|
{
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
if self.i == s {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
std::str::from_utf8(&self.b[s..self.i])
|
||||||
|
.ok()?
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
.map(Val::Int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn section(&mut self, top: bool) -> Option<Section> {
|
||||||
|
let mut sec = Section::default();
|
||||||
|
loop {
|
||||||
|
self.skip();
|
||||||
|
if self.i >= self.b.len() {
|
||||||
|
return top.then_some(sec);
|
||||||
|
}
|
||||||
|
if self.b[self.i] == b'}' {
|
||||||
|
self.i += 1;
|
||||||
|
return (!top).then_some(sec);
|
||||||
|
}
|
||||||
|
let name = self.ident()?;
|
||||||
|
self.skip();
|
||||||
|
match *self.b.get(self.i)? {
|
||||||
|
b'=' => {
|
||||||
|
self.i += 1;
|
||||||
|
let v = self.value()?;
|
||||||
|
sec.values.insert(name, v);
|
||||||
|
}
|
||||||
|
b'{' => {
|
||||||
|
self.i += 1;
|
||||||
|
let s = self.section(false)?;
|
||||||
|
sec.sections.push((name, s));
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(text: &str) -> Option<Section> {
|
||||||
|
Lexer {
|
||||||
|
b: text.as_bytes(),
|
||||||
|
i: 0,
|
||||||
|
}
|
||||||
|
.section(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- montagem
|
||||||
|
|
||||||
|
fn dashed(u: &str) -> String {
|
||||||
|
if u.len() != 32 {
|
||||||
|
return u.to_string();
|
||||||
|
}
|
||||||
|
format!(
|
||||||
|
"{}-{}-{}-{}-{}-{}-{}",
|
||||||
|
&u[0..6],
|
||||||
|
&u[6..10],
|
||||||
|
&u[10..14],
|
||||||
|
&u[14..18],
|
||||||
|
&u[18..22],
|
||||||
|
&u[22..26],
|
||||||
|
&u[26..32]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum Map {
|
||||||
|
/// Lê de `src` a partir de `base` (linear).
|
||||||
|
Linear {
|
||||||
|
src: Option<Arc<dyn Source>>,
|
||||||
|
base: u64,
|
||||||
|
},
|
||||||
|
Striped {
|
||||||
|
srcs: Vec<(Option<Arc<dyn Source>>, u64)>,
|
||||||
|
stripe: u64,
|
||||||
|
},
|
||||||
|
Mirror {
|
||||||
|
legs: Vec<(Option<Arc<dyn Source>>, u64)>,
|
||||||
|
},
|
||||||
|
Raid(Arc<RaidSource>),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Seg {
|
||||||
|
start: u64,
|
||||||
|
len: u64,
|
||||||
|
map: Map,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LvSource {
|
||||||
|
segs: Vec<Seg>,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for LvSource {
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.size || buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let want = buf.len().min((self.size - offset) as usize);
|
||||||
|
let mut done = 0;
|
||||||
|
while done < want {
|
||||||
|
let pos = offset + done as u64;
|
||||||
|
let i = self.segs.partition_point(|s| s.start + s.len <= pos);
|
||||||
|
let Some(s) = self.segs.get(i) else { break };
|
||||||
|
let rel = pos - s.start;
|
||||||
|
let mut n = ((s.len - rel) as usize).min(want - done);
|
||||||
|
let dst = &mut buf[done..done + n];
|
||||||
|
match &s.map {
|
||||||
|
Map::Linear { src, base } => match src {
|
||||||
|
Some(src) => n = read_full(src.as_ref(), base + rel, dst)?,
|
||||||
|
None => dst.fill(0),
|
||||||
|
},
|
||||||
|
Map::Striped { srcs, stripe } => {
|
||||||
|
let ns = srcs.len() as u64;
|
||||||
|
let (idx, within) = (rel / stripe, rel % stripe);
|
||||||
|
n = n.min((stripe - within) as usize);
|
||||||
|
let (src, base) = &srcs[(idx % ns) as usize];
|
||||||
|
let off = base + (idx / ns) * stripe + within;
|
||||||
|
match src {
|
||||||
|
Some(src) => n = read_full(src.as_ref(), off, &mut dst[..n])?,
|
||||||
|
None => dst[..n].fill(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map::Mirror { legs } => {
|
||||||
|
let mut ok = false;
|
||||||
|
for (src, base) in legs {
|
||||||
|
if let Some(src) = src {
|
||||||
|
if let Ok(g) = read_full(src.as_ref(), base + rel, dst) {
|
||||||
|
n = g;
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
dst.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map::Raid(r) => n = read_full(r.as_ref(), rel, dst)?,
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_full(src: &dyn Source, off: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let mut got = 0;
|
||||||
|
while got < buf.len() {
|
||||||
|
let n = src.read_at(off + got as u64, &mut buf[got..])?;
|
||||||
|
if n == 0 {
|
||||||
|
buf[got..].fill(0);
|
||||||
|
return Ok(buf.len());
|
||||||
|
}
|
||||||
|
got += n;
|
||||||
|
}
|
||||||
|
Ok(got)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VgResult {
|
||||||
|
pub name: String,
|
||||||
|
pub lvs: Vec<(String, Arc<dyn Source>)>,
|
||||||
|
pub warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mk_stripes(list: &[Val]) -> Vec<(String, u64)> {
|
||||||
|
let mut out = vec![];
|
||||||
|
for pair in list.chunks(2) {
|
||||||
|
if let [Val::Str(n), Val::Int(e)] = pair {
|
||||||
|
out.push((n.clone(), *e as u64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Ctx<'a> {
|
||||||
|
ext: u64,
|
||||||
|
pvs: BTreeMap<String, (Option<Arc<dyn Source>>, u64)>, // nome -> (origem, pe_start em bytes)
|
||||||
|
lvs: &'a Section,
|
||||||
|
cache: BTreeMap<String, Option<Arc<dyn Source>>>,
|
||||||
|
warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ctx<'_> {
|
||||||
|
fn build(&mut self, name: &str, depth: u32) -> Option<Arc<dyn Source>> {
|
||||||
|
if let Some(c) = self.cache.get(name) {
|
||||||
|
return c.clone();
|
||||||
|
}
|
||||||
|
if depth > 6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let lv = self.lvs.sub(name)?.clone();
|
||||||
|
let nsegs = lv.int("segment_count").unwrap_or(0);
|
||||||
|
let mut segs: Vec<Seg> = vec![];
|
||||||
|
for i in 1..=nsegs {
|
||||||
|
let Some(seg) = lv.sub(&format!("segment{i}")) else {
|
||||||
|
self.warnings.push(format!("{name}: segmento {i} ausente"));
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let start = seg.int("start_extent").unwrap_or(0) * self.ext;
|
||||||
|
let len = seg.int("extent_count").unwrap_or(0) * self.ext;
|
||||||
|
let ty = seg.str("type").unwrap_or("").to_string();
|
||||||
|
let map = match ty.as_str() {
|
||||||
|
"striped" => {
|
||||||
|
let st = mk_stripes(seg.list("stripes").unwrap_or(&[]));
|
||||||
|
let srcs: Vec<(Option<Arc<dyn Source>>, u64)> = st
|
||||||
|
.iter()
|
||||||
|
.map(|(pv, e)| match self.pvs.get(pv) {
|
||||||
|
Some((s, pe)) => (s.clone(), pe + e * self.ext),
|
||||||
|
None => (None, 0),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if srcs.iter().any(|(s, _)| s.is_none()) {
|
||||||
|
self.warnings.push(format!(
|
||||||
|
"{name}: falta um volume físico; as áreas dele aparecem zeradas"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if srcs.len() == 1 {
|
||||||
|
let (src, base) = srcs.into_iter().next().unwrap();
|
||||||
|
Map::Linear { src, base }
|
||||||
|
} else {
|
||||||
|
Map::Striped {
|
||||||
|
srcs,
|
||||||
|
stripe: seg.int("stripe_size").unwrap_or(128) * 512,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"mirror" => {
|
||||||
|
let st = mk_stripes(seg.list("mirrors").unwrap_or(&[]));
|
||||||
|
let legs = st
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(pv, e)| {
|
||||||
|
self.pvs
|
||||||
|
.get(pv)
|
||||||
|
.map(|(s, pe)| (s.clone(), pe + e * self.ext))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Map::Mirror { legs }
|
||||||
|
}
|
||||||
|
t if t.starts_with("raid") => {
|
||||||
|
let raids = seg.list("raids").unwrap_or(&[]);
|
||||||
|
let imgs: Vec<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.
|
||||||
|
let data_lvs: Vec<&String> =
|
||||||
|
imgs.iter().filter(|n| n.contains("_rimage_")).collect();
|
||||||
|
let mut members: Vec<Option<Member>> = vec![];
|
||||||
|
for n in &data_lvs {
|
||||||
|
members.push(self.build(n, depth + 1).map(|src| Member {
|
||||||
|
src,
|
||||||
|
data_offset: 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
let chunk = seg.int("stripe_size").unwrap_or(128) * 512;
|
||||||
|
let (level, layout) = match t {
|
||||||
|
"raid1" => (Level::R1, Layout::LeftSymmetric),
|
||||||
|
"raid0" | "raid0_meta" => (Level::R0, Layout::LeftSymmetric),
|
||||||
|
"raid4" | "raid5_n" => (Level::R4, Layout::ParityLast),
|
||||||
|
"raid5" | "raid5_ls" => (Level::R5, Layout::LeftSymmetric),
|
||||||
|
"raid5_la" => (Level::R5, Layout::LeftAsymmetric),
|
||||||
|
"raid5_rs" => (Level::R5, Layout::RightSymmetric),
|
||||||
|
"raid5_ra" => (Level::R5, Layout::RightAsymmetric),
|
||||||
|
"raid6" | "raid6_zr" | "raid6_ls_6" => (Level::R6, Layout::LeftSymmetric),
|
||||||
|
"raid6_nr" => (Level::R6, Layout::LeftAsymmetric),
|
||||||
|
"raid6_nc" | "raid6_n_6" => (Level::R6, Layout::ParityLast),
|
||||||
|
"raid10" => (Level::R10, Layout::LeftSymmetric),
|
||||||
|
other => {
|
||||||
|
self.warnings
|
||||||
|
.push(format!("{name}: tipo {other} não suportado"));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut p = RaidParams::simple(level, chunk.max(512), layout, members);
|
||||||
|
p.near = 2;
|
||||||
|
p.far = 1;
|
||||||
|
match RaidSource::new(p) {
|
||||||
|
Ok(r) => Map::Raid(Arc::new(r)),
|
||||||
|
Err(e) => {
|
||||||
|
self.warnings.push(format!("{name}: {e}"));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
self.warnings.push(format!(
|
||||||
|
"{name}: tipo de segmento \"{other}\" não suportado"
|
||||||
|
));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
segs.push(Seg { start, len, map });
|
||||||
|
}
|
||||||
|
segs.sort_by_key(|s| s.start);
|
||||||
|
let size = segs.iter().map(|s| s.start + s.len).max().unwrap_or(0);
|
||||||
|
let src: Arc<dyn Source> = Arc::new(LvSource { segs, size });
|
||||||
|
self.cache.insert(name.to_string(), Some(src.clone()));
|
||||||
|
Some(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Monta os volumes lógicos de cada grupo de volumes presente nos PVs fornecidos.
|
||||||
|
pub fn assemble_vgs(pvs: Vec<(Arc<dyn Source>, PvInfo)>) -> Vec<VgResult> {
|
||||||
|
let mut by_vg: BTreeMap<String, Vec<(Arc<dyn Source>, PvInfo)>> = BTreeMap::new();
|
||||||
|
for (s, p) in pvs {
|
||||||
|
by_vg.entry(p.vg_name.clone()).or_default().push((s, p));
|
||||||
|
}
|
||||||
|
let mut out = vec![];
|
||||||
|
for (vg_name, list) in by_vg {
|
||||||
|
let newest = list.iter().max_by_key(|(_, p)| p.seqno).unwrap();
|
||||||
|
let Some(tree) = parse(&newest.1.text) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(vg) = tree.sub(&vg_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let ext = vg.int("extent_size").unwrap_or(8192) * 512;
|
||||||
|
let mut pvmap = BTreeMap::new();
|
||||||
|
let mut warnings = vec![];
|
||||||
|
if let Some(pvsec) = vg.sub("physical_volumes") {
|
||||||
|
for (pvname, def) in &pvsec.sections {
|
||||||
|
let id = def.str("id").unwrap_or("").to_string();
|
||||||
|
let found = list
|
||||||
|
.iter()
|
||||||
|
.find(|(_, p)| dashed(&p.uuid) == id)
|
||||||
|
.map(|(s, _)| s.clone());
|
||||||
|
if found.is_none() {
|
||||||
|
warnings.push(format!("volume físico {pvname} ({id}) não foi fornecido"));
|
||||||
|
}
|
||||||
|
pvmap.insert(
|
||||||
|
pvname.clone(),
|
||||||
|
(found, def.int("pe_start").unwrap_or(2048) * 512),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(lvsec) = vg.sub("logical_volumes") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut ctx = Ctx {
|
||||||
|
ext,
|
||||||
|
pvs: pvmap,
|
||||||
|
lvs: lvsec,
|
||||||
|
cache: BTreeMap::new(),
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
let mut lvs = vec![];
|
||||||
|
for (name, def) in &lvsec.sections {
|
||||||
|
// Imagens internas do RAID e metadados não são volumes de usuário.
|
||||||
|
let hidden = def
|
||||||
|
.list("status")
|
||||||
|
.map(|l| !l.iter().any(|v| matches!(v, Val::Str(s) if s == "VISIBLE")))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if hidden {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(src) = ctx.build(name, 0) {
|
||||||
|
lvs.push((name.clone(), src));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(VgResult {
|
||||||
|
name: vg_name,
|
||||||
|
lvs,
|
||||||
|
warnings: ctx.warnings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -13,7 +13,24 @@ pub fn detect_fs(src: &dyn Source, start: u64) -> Option<(FsKind, Option<u64>)>
|
|||||||
if b.len() < 1100 {
|
if b.len() < 1100 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
detect_fs_in(&b)
|
if let Some(r) = detect_fs_in(&b) {
|
||||||
|
return Some(r);
|
||||||
|
}
|
||||||
|
// Assinaturas mais adiante no volume: Btrfs (64 KiB) e RAID mdadm 1.2 (4 KiB).
|
||||||
|
if let Ok(h) = read_vec(src, start + 0x10000, 0x100) {
|
||||||
|
if h.len() >= 0x80 && &h[0x40..0x48] == b"_BHRfS_M" {
|
||||||
|
return Some((
|
||||||
|
FsKind::Btrfs,
|
||||||
|
Some(u64::from_le_bytes(h[0x70..0x78].try_into().unwrap())),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(h) = read_vec(src, start + 4096, 16) {
|
||||||
|
if h.len() >= 4 && h[..4] == [0xFC, 0x4E, 0x2B, 0xA9] {
|
||||||
|
return Some((FsKind::Raid, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mesma detecção sobre um buffer que começa no setor candidato (>= 1100 bytes).
|
/// Mesma detecção sobre um buffer que começa no setor candidato (>= 1100 bytes).
|
||||||
@@ -35,7 +52,31 @@ pub fn detect_fs_in(b: &[u8]) -> Option<(FsKind, Option<u64>)> {
|
|||||||
return Some((FsKind::Exfat, Some(vol << shift)));
|
return Some((FsKind::Exfat, Some(vol << shift)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if &b[3..11] == b"-FVE-FS-" {
|
if b.len() >= 520 && &b[512..520] == b"LABELONE" {
|
||||||
|
return Some((FsKind::Lvm, None));
|
||||||
|
}
|
||||||
|
if b[..4] == [0xFC, 0x4E, 0x2B, 0xA9] {
|
||||||
|
return Some((FsKind::Raid, None));
|
||||||
|
}
|
||||||
|
if &b[0..4] == b"XFSB" {
|
||||||
|
let bs = u32::from_be_bytes(b[4..8].try_into().unwrap()) as u64;
|
||||||
|
let blocks = u64::from_be_bytes(b[8..16].try_into().unwrap());
|
||||||
|
if bs.is_power_of_two() && (512..=65536).contains(&bs) {
|
||||||
|
return Some((FsKind::Xfs, Some(bs * blocks)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if &b[3..7] == b"ReFS" && &b[16..20] == b"FSRS" {
|
||||||
|
let sectors = u64::from_le_bytes(b[24..32].try_into().unwrap());
|
||||||
|
let bps = u32::from_le_bytes(b[32..36].try_into().unwrap()) as u64;
|
||||||
|
return Some((FsKind::Refs, Some(sectors * bps)));
|
||||||
|
}
|
||||||
|
if crate::crypt::filevault::detect(b) {
|
||||||
|
return Some((
|
||||||
|
FsKind::Corestorage,
|
||||||
|
Some(u64::from_le_bytes(b[64..72].try_into().unwrap())),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if crate::crypt::bitlocker::detect(b) {
|
||||||
return Some((FsKind::Bitlocker, None));
|
return Some((FsKind::Bitlocker, None));
|
||||||
}
|
}
|
||||||
if b.starts_with(b"LUKS\xBA\xBE") {
|
if b.starts_with(b"LUKS\xBA\xBE") {
|
||||||
@@ -213,7 +254,7 @@ pub fn analyze(src: &dyn Source, search_lost: bool) -> DiskLayout {
|
|||||||
for p in &layout.partitions {
|
for p in &layout.partitions {
|
||||||
if let Some(e) = &p.encrypted {
|
if let Some(e) = &p.encrypted {
|
||||||
layout.warnings.push(format!(
|
layout.warnings.push(format!(
|
||||||
"Partição {} está cifrada com {}: o Guará só detecta cifragem na v1 e não varre esse volume sem a chave.",
|
"Partição {} está cifrada com {}: informe a senha, a chave de recuperação ou o arquivo de chave para abrir e varrer o conteúdo.",
|
||||||
p.index + 1,
|
p.index + 1,
|
||||||
e
|
e
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -0,0 +1,759 @@
|
|||||||
|
//! RAID por software: linear, 0, 1, 4, 5, 6 e 10, a partir dos discos soltos.
|
||||||
|
//!
|
||||||
|
//! Lê os metadados do mdadm (superbloco 0.90 e 1.x) para montar sozinho; sem metadados, o
|
||||||
|
//! módulo `raid_detect` descobre ordem, tamanho de faixa e layout. Se faltarem discos, o
|
||||||
|
//! array é lido em modo degradado (paridade reconstrói o que faltar).
|
||||||
|
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Level {
|
||||||
|
Linear,
|
||||||
|
R0,
|
||||||
|
R1,
|
||||||
|
R4,
|
||||||
|
R5,
|
||||||
|
R6,
|
||||||
|
R10,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Level {
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Level::Linear => "linear",
|
||||||
|
Level::R0 => "RAID 0",
|
||||||
|
Level::R1 => "RAID 1",
|
||||||
|
Level::R4 => "RAID 4",
|
||||||
|
Level::R5 => "RAID 5",
|
||||||
|
Level::R6 => "RAID 6",
|
||||||
|
Level::R10 => "RAID 10",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Posição da paridade e ordem dos dados, como no driver md do Linux.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum Layout {
|
||||||
|
LeftAsymmetric,
|
||||||
|
RightAsymmetric,
|
||||||
|
LeftSymmetric,
|
||||||
|
RightSymmetric,
|
||||||
|
/// RAID 4 e variantes "parity-0"/"parity-n".
|
||||||
|
ParityFirst,
|
||||||
|
ParityLast,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
pub fn from_md(v: u32) -> Layout {
|
||||||
|
match v {
|
||||||
|
0 => Layout::LeftAsymmetric,
|
||||||
|
1 => Layout::RightAsymmetric,
|
||||||
|
2 => Layout::LeftSymmetric,
|
||||||
|
3 => Layout::RightSymmetric,
|
||||||
|
4 => Layout::ParityFirst,
|
||||||
|
5 => Layout::ParityLast,
|
||||||
|
_ => Layout::LeftSymmetric,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn all_raid5() -> [Layout; 4] {
|
||||||
|
[
|
||||||
|
Layout::LeftSymmetric,
|
||||||
|
Layout::LeftAsymmetric,
|
||||||
|
Layout::RightSymmetric,
|
||||||
|
Layout::RightAsymmetric,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Member {
|
||||||
|
pub src: Arc<dyn Source>,
|
||||||
|
/// Onde começam os dados dentro do membro.
|
||||||
|
pub data_offset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RaidParams {
|
||||||
|
pub level: Level,
|
||||||
|
/// Tamanho da faixa por disco, em bytes.
|
||||||
|
pub chunk: u64,
|
||||||
|
pub layout: Layout,
|
||||||
|
/// Membros na ordem do array; `None` = disco ausente.
|
||||||
|
pub members: Vec<Option<Member>>,
|
||||||
|
/// RAID 10: cópias "perto" (near) e "longe" (far).
|
||||||
|
pub near: u32,
|
||||||
|
pub far: u32,
|
||||||
|
pub far_offset: bool,
|
||||||
|
/// Bytes úteis por membro (0 = até o fim do menor).
|
||||||
|
pub per_member: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RaidParams {
|
||||||
|
pub fn simple(level: Level, chunk: u64, layout: Layout, members: Vec<Option<Member>>) -> Self {
|
||||||
|
Self {
|
||||||
|
level,
|
||||||
|
chunk,
|
||||||
|
layout,
|
||||||
|
members,
|
||||||
|
near: 2,
|
||||||
|
far: 1,
|
||||||
|
far_offset: false,
|
||||||
|
per_member: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RaidSource {
|
||||||
|
p: RaidParams,
|
||||||
|
size: u64,
|
||||||
|
per_member: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ GF(2^8) para RAID 6
|
||||||
|
|
||||||
|
fn gf_mul(mut a: u8, mut b: u8) -> u8 {
|
||||||
|
let mut r = 0u8;
|
||||||
|
while b != 0 {
|
||||||
|
if b & 1 != 0 {
|
||||||
|
r ^= a;
|
||||||
|
}
|
||||||
|
let hi = a & 0x80;
|
||||||
|
a <<= 1;
|
||||||
|
if hi != 0 {
|
||||||
|
a ^= 0x1d;
|
||||||
|
}
|
||||||
|
b >>= 1;
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gf_pow2(n: usize) -> u8 {
|
||||||
|
let mut r = 1u8;
|
||||||
|
for _ in 0..n % 255 {
|
||||||
|
r = gf_mul(r, 2);
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gf_inv(a: u8) -> u8 {
|
||||||
|
// a^254
|
||||||
|
let mut r = 1u8;
|
||||||
|
let mut base = a;
|
||||||
|
let mut e = 254u32;
|
||||||
|
while e > 0 {
|
||||||
|
if e & 1 != 0 {
|
||||||
|
r = gf_mul(r, base);
|
||||||
|
}
|
||||||
|
base = gf_mul(base, base);
|
||||||
|
e >>= 1;
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RaidSource {
|
||||||
|
pub fn new(p: RaidParams) -> Result<Self, String> {
|
||||||
|
let n = p.members.len();
|
||||||
|
if n == 0 {
|
||||||
|
return Err("RAID sem membros".into());
|
||||||
|
}
|
||||||
|
let present: Vec<&Member> = p.members.iter().flatten().collect();
|
||||||
|
let min = |f: &dyn Fn(&Member) -> u64| present.iter().map(|m| f(m)).min().unwrap_or(0);
|
||||||
|
let avail = min(&|m| m.src.size().saturating_sub(m.data_offset));
|
||||||
|
let mut per_member = if p.per_member > 0 {
|
||||||
|
p.per_member.min(avail)
|
||||||
|
} else {
|
||||||
|
avail
|
||||||
|
};
|
||||||
|
if p.level != Level::Linear {
|
||||||
|
per_member = per_member / p.chunk.max(512) * p.chunk.max(512);
|
||||||
|
}
|
||||||
|
let missing = n - present.len();
|
||||||
|
let need = match p.level {
|
||||||
|
Level::Linear | Level::R0 => 0,
|
||||||
|
Level::R1 => n - 1,
|
||||||
|
Level::R4 | Level::R5 => 1,
|
||||||
|
Level::R6 => 2,
|
||||||
|
Level::R10 => (p.near.max(1) as usize).saturating_sub(1).min(1),
|
||||||
|
};
|
||||||
|
if missing > need && p.level != Level::R10 {
|
||||||
|
return Err(format!(
|
||||||
|
"faltam {missing} membro(s); {} tolera no máximo {need}",
|
||||||
|
p.level.name()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if matches!(p.level, Level::R4 | Level::R5) && n < 3 || p.level == Level::R6 && n < 4 {
|
||||||
|
return Err("poucos membros para o nível".into());
|
||||||
|
}
|
||||||
|
if p.level == Level::R10 && (n < 2 || p.near as usize * p.far as usize > n.max(2)) {
|
||||||
|
return Err("parâmetros de RAID 10 inválidos".into());
|
||||||
|
}
|
||||||
|
let size = match p.level {
|
||||||
|
Level::Linear => present
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.src.size().saturating_sub(m.data_offset))
|
||||||
|
.sum(),
|
||||||
|
Level::R0 => per_member * n as u64,
|
||||||
|
Level::R1 => per_member,
|
||||||
|
Level::R4 | Level::R5 => per_member * (n as u64 - 1),
|
||||||
|
Level::R6 => per_member * (n as u64 - 2),
|
||||||
|
Level::R10 => per_member * n as u64 / (p.near.max(1) * p.far.max(1)) as u64,
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
p,
|
||||||
|
size,
|
||||||
|
per_member,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn params(&self) -> &RaidParams {
|
||||||
|
&self.p
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_member(&self, i: usize, off: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||||
|
let m = self.p.members[i]
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "membro ausente"))?;
|
||||||
|
let mut done = 0;
|
||||||
|
while done < buf.len() {
|
||||||
|
let n = m
|
||||||
|
.src
|
||||||
|
.read_at(m.data_offset + off + done as u64, &mut buf[done..])?;
|
||||||
|
if n == 0 {
|
||||||
|
buf[done..].fill(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (disco de paridade P, disco de paridade Q, disco que guarda o dado `d` da linha `row`)
|
||||||
|
fn raid5_map(&self, row: u64, d: usize) -> (usize, usize) {
|
||||||
|
let n = self.p.members.len();
|
||||||
|
let r = (row % n as u64) as usize;
|
||||||
|
match self.p.layout {
|
||||||
|
Layout::LeftAsymmetric | Layout::LeftSymmetric => {
|
||||||
|
let pd = n - 1 - r;
|
||||||
|
let disk = if self.p.layout == Layout::LeftSymmetric {
|
||||||
|
(pd + 1 + d) % n
|
||||||
|
} else if d >= pd {
|
||||||
|
d + 1
|
||||||
|
} else {
|
||||||
|
d
|
||||||
|
};
|
||||||
|
(pd, disk)
|
||||||
|
}
|
||||||
|
Layout::RightAsymmetric | Layout::RightSymmetric => {
|
||||||
|
let pd = r;
|
||||||
|
let disk = if self.p.layout == Layout::RightSymmetric {
|
||||||
|
(pd + 1 + d) % n
|
||||||
|
} else if d >= pd {
|
||||||
|
d + 1
|
||||||
|
} else {
|
||||||
|
d
|
||||||
|
};
|
||||||
|
(pd, disk)
|
||||||
|
}
|
||||||
|
Layout::ParityFirst => (0, d + 1),
|
||||||
|
Layout::ParityLast => (n - 1, d),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raid6_map(&self, row: u64, d: usize) -> (usize, usize, usize) {
|
||||||
|
let n = self.p.members.len();
|
||||||
|
let r = (row % n as u64) as usize;
|
||||||
|
match self.p.layout {
|
||||||
|
Layout::LeftAsymmetric | Layout::LeftSymmetric => {
|
||||||
|
let pd = n - 1 - r;
|
||||||
|
let qd = (pd + 1) % n;
|
||||||
|
let disk = if self.p.layout == Layout::LeftSymmetric {
|
||||||
|
(pd + 2 + d) % n
|
||||||
|
} else {
|
||||||
|
// assimétrico: dados em ordem pulando P e Q
|
||||||
|
let mut k = d;
|
||||||
|
for skip in sorted2(pd, qd) {
|
||||||
|
if k >= skip {
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k
|
||||||
|
};
|
||||||
|
(pd, qd, disk)
|
||||||
|
}
|
||||||
|
Layout::RightAsymmetric | Layout::RightSymmetric => {
|
||||||
|
let pd = r;
|
||||||
|
let qd = (pd + 1) % n;
|
||||||
|
let disk = if self.p.layout == Layout::RightSymmetric {
|
||||||
|
(pd + 2 + d) % n
|
||||||
|
} else {
|
||||||
|
let mut k = d;
|
||||||
|
for skip in sorted2(pd, qd) {
|
||||||
|
if k >= skip {
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k
|
||||||
|
};
|
||||||
|
(pd, qd, disk)
|
||||||
|
}
|
||||||
|
Layout::ParityFirst => (0, 1, d + 2),
|
||||||
|
Layout::ParityLast => (n - 2, n - 1, d),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lê `buf.len()` bytes dentro de um único chunk lógico.
|
||||||
|
fn read_in_chunk(&self, chunk_idx: u64, within: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||||
|
let n = self.p.members.len();
|
||||||
|
let c = self.p.chunk;
|
||||||
|
match self.p.level {
|
||||||
|
Level::Linear => unreachable!(),
|
||||||
|
Level::R0 => {
|
||||||
|
let (row, disk) = (chunk_idx / n as u64, (chunk_idx % n as u64) as usize);
|
||||||
|
self.read_member(disk, row * c + within, buf)
|
||||||
|
}
|
||||||
|
Level::R1 => {
|
||||||
|
for i in 0..n {
|
||||||
|
if self.p.members[i].is_some()
|
||||||
|
&& self.read_member(i, chunk_idx * c + within, buf).is_ok()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(io::Error::other("nenhuma cópia legível"))
|
||||||
|
}
|
||||||
|
Level::R4 | Level::R5 => {
|
||||||
|
let dd = n - 1;
|
||||||
|
let (row, d) = (chunk_idx / dd as u64, (chunk_idx % dd as u64) as usize);
|
||||||
|
let (pd, disk) = self.raid5_map(row, d);
|
||||||
|
let off = row * c + within;
|
||||||
|
if self.p.members[disk].is_some() {
|
||||||
|
return self.read_member(disk, off, buf);
|
||||||
|
}
|
||||||
|
// Reconstrói por XOR de todos os outros (dados + paridade) da linha.
|
||||||
|
buf.fill(0);
|
||||||
|
let mut tmp = vec![0u8; buf.len()];
|
||||||
|
for i in 0..n {
|
||||||
|
if i == disk {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = pd;
|
||||||
|
self.read_member(i, off, &mut tmp)?;
|
||||||
|
for (a, b) in buf.iter_mut().zip(&tmp) {
|
||||||
|
*a ^= *b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Level::R6 => {
|
||||||
|
let dd = n - 2;
|
||||||
|
let (row, d) = (chunk_idx / dd as u64, (chunk_idx % dd as u64) as usize);
|
||||||
|
let (_, _, disk) = self.raid6_map(row, d);
|
||||||
|
let off = row * c + within;
|
||||||
|
if self.p.members[disk].is_some() {
|
||||||
|
return self.read_member(disk, off, buf);
|
||||||
|
}
|
||||||
|
self.raid6_rebuild(row, d, off, buf)
|
||||||
|
}
|
||||||
|
Level::R10 => {
|
||||||
|
let mut last = None;
|
||||||
|
for (disk, off) in self.r10_locations(chunk_idx) {
|
||||||
|
if self.p.members[disk].is_some() {
|
||||||
|
match self.read_member(disk, off + within, buf) {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(e) => last = Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(last
|
||||||
|
.unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "cópias ausentes")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Onde ficam todas as cópias de um chunk lógico (como `raid10_find_phys` do kernel).
|
||||||
|
fn r10_locations(&self, chunk_idx: u64) -> Vec<(usize, u64)> {
|
||||||
|
let n = self.p.members.len() as u64;
|
||||||
|
let c = self.p.chunk;
|
||||||
|
let near = self.p.near.max(1) as u64;
|
||||||
|
let far = self.p.far.max(1) as u64;
|
||||||
|
let slot0 = chunk_idx * near;
|
||||||
|
let (mut stripe, dev) = (slot0 / n, slot0 % n);
|
||||||
|
if self.p.far_offset {
|
||||||
|
stripe *= far;
|
||||||
|
}
|
||||||
|
let stride = if self.p.far_offset {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
(self.per_member / far) / c * c
|
||||||
|
};
|
||||||
|
let mut out = vec![];
|
||||||
|
let mut d = dev;
|
||||||
|
let mut off = stripe * c;
|
||||||
|
let mut firsts = vec![];
|
||||||
|
for _ in 0..near {
|
||||||
|
firsts.push((d, off));
|
||||||
|
out.push((d as usize, off));
|
||||||
|
d += 1;
|
||||||
|
if d >= n {
|
||||||
|
d = 0;
|
||||||
|
off += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for f in 1..far {
|
||||||
|
for &(dv, of) in &firsts {
|
||||||
|
out.push((((dv + near * f) % n) as usize, of + stride * f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raid6_rebuild(&self, row: u64, d: usize, off: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||||
|
let n = self.p.members.len();
|
||||||
|
let dd = n - 2;
|
||||||
|
let (pd, qd, _) = self.raid6_map(row, 0);
|
||||||
|
let len = buf.len();
|
||||||
|
// Estado de cada dado da linha.
|
||||||
|
let disks: Vec<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 p_ok = self.p.members[pd].is_some();
|
||||||
|
let q_ok = self.p.members[qd].is_some();
|
||||||
|
let mut data: Vec<Option<Vec<u8>>> = vec![None; dd];
|
||||||
|
for k in 0..dd {
|
||||||
|
if self.p.members[disks[k]].is_some() {
|
||||||
|
let mut v = vec![0u8; len];
|
||||||
|
self.read_member(disks[k], off, &mut v)?;
|
||||||
|
data[k] = Some(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rd = |disk: usize| -> io::Result<Vec<u8>> {
|
||||||
|
let mut v = vec![0u8; len];
|
||||||
|
self.read_member(disk, off, &mut v)?;
|
||||||
|
Ok(v)
|
||||||
|
};
|
||||||
|
if missing_data.len() == 1 && p_ok {
|
||||||
|
let p = rd(pd)?;
|
||||||
|
let mut acc = p;
|
||||||
|
for v in data.iter().flatten() {
|
||||||
|
for (a, b) in acc.iter_mut().zip(v) {
|
||||||
|
*a ^= *b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.copy_from_slice(&acc);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if missing_data.len() == 1 && q_ok {
|
||||||
|
let q = rd(qd)?;
|
||||||
|
let x = missing_data[0];
|
||||||
|
let mut acc = q;
|
||||||
|
for (k, v) in data.iter().enumerate() {
|
||||||
|
if let Some(v) = v {
|
||||||
|
let g = gf_pow2(k);
|
||||||
|
for (a, b) in acc.iter_mut().zip(v) {
|
||||||
|
*a ^= gf_mul(g, *b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let inv = gf_inv(gf_pow2(x));
|
||||||
|
for (o, a) in buf.iter_mut().zip(&acc) {
|
||||||
|
*o = gf_mul(inv, *a);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if missing_data.len() == 2 && p_ok && q_ok {
|
||||||
|
let (x, y) = (missing_data[0], missing_data[1]);
|
||||||
|
let p = rd(pd)?;
|
||||||
|
let q = rd(qd)?;
|
||||||
|
let (mut pxy, mut qxy) = (p, q);
|
||||||
|
for (k, v) in data.iter().enumerate() {
|
||||||
|
if let Some(v) = v {
|
||||||
|
let g = gf_pow2(k);
|
||||||
|
for i in 0..len {
|
||||||
|
pxy[i] ^= v[i];
|
||||||
|
qxy[i] ^= gf_mul(g, v[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dx ^ Dy = pxy ; g^x Dx ^ g^y Dy = qxy
|
||||||
|
let (gx, gy) = (gf_pow2(x), gf_pow2(y));
|
||||||
|
let denom_inv = gf_inv(gx ^ gy);
|
||||||
|
let target = if d == x { x } else { y };
|
||||||
|
for i in 0..len {
|
||||||
|
// Dx = (qxy ^ gy*pxy) / (gx ^ gy)
|
||||||
|
let dx = gf_mul(denom_inv, qxy[i] ^ gf_mul(gy, pxy[i]));
|
||||||
|
buf[i] = if target == x { dx } else { pxy[i] ^ dx };
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(io::Error::other(
|
||||||
|
"RAID 6 sem redundância suficiente nesta linha",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sorted2(a: usize, b: usize) -> [usize; 2] {
|
||||||
|
if a < b {
|
||||||
|
[a, b]
|
||||||
|
} else {
|
||||||
|
[b, a]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for RaidSource {
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.size || buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let want = buf.len().min((self.size - offset) as usize);
|
||||||
|
let mut done = 0usize;
|
||||||
|
if self.p.level == Level::Linear {
|
||||||
|
let mut base = 0u64;
|
||||||
|
for m in self.p.members.iter().flatten() {
|
||||||
|
let len = m.src.size().saturating_sub(m.data_offset);
|
||||||
|
if offset + done as u64 >= base && offset + (done as u64) < base + len {
|
||||||
|
let rel = offset + done as u64 - base;
|
||||||
|
let n = ((len - rel) as usize).min(want - done);
|
||||||
|
let got = m
|
||||||
|
.src
|
||||||
|
.read_at(m.data_offset + rel, &mut buf[done..done + n])?;
|
||||||
|
if got == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done += got;
|
||||||
|
if done >= want {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base += len;
|
||||||
|
}
|
||||||
|
return Ok(done);
|
||||||
|
}
|
||||||
|
let c = self.p.chunk;
|
||||||
|
while done < want {
|
||||||
|
let pos = offset + done as u64;
|
||||||
|
let (idx, within) = (pos / c, pos % c);
|
||||||
|
let n = ((c - within) as usize).min(want - done);
|
||||||
|
self.read_in_chunk(idx, within, &mut buf[done..done + n])?;
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================ metadados do mdadm
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MdSuper {
|
||||||
|
pub version: String,
|
||||||
|
pub uuid: [u8; 16],
|
||||||
|
pub name: String,
|
||||||
|
pub level: Level,
|
||||||
|
pub layout: u32,
|
||||||
|
pub chunk: u64,
|
||||||
|
pub raid_disks: u32,
|
||||||
|
/// Papel deste disco no array (None = reserva/falho).
|
||||||
|
pub role: Option<u32>,
|
||||||
|
pub data_offset: u64,
|
||||||
|
/// Bytes de dados por membro.
|
||||||
|
pub data_size: u64,
|
||||||
|
pub events: u64,
|
||||||
|
pub array_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn le32(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn le64(b: &[u8], o: usize) -> u64 {
|
||||||
|
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn level_from(v: i32) -> Option<Level> {
|
||||||
|
Some(match v {
|
||||||
|
-1 => Level::Linear,
|
||||||
|
0 => Level::R0,
|
||||||
|
1 => Level::R1,
|
||||||
|
4 => Level::R4,
|
||||||
|
5 => Level::R5,
|
||||||
|
6 => Level::R6,
|
||||||
|
10 => Level::R10,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Procura um superbloco md nas posições possíveis de `src` (um disco ou partição).
|
||||||
|
pub fn read_md_super(src: &dyn Source) -> Option<MdSuper> {
|
||||||
|
let size = src.size();
|
||||||
|
// 1.1 no início, 1.2 a 4 KiB, 1.0 a 8 KiB do fim.
|
||||||
|
let mut cands: Vec<(u64, &str)> = vec![(0, "1.1"), (4096, "1.2")];
|
||||||
|
if size > 16384 {
|
||||||
|
cands.push(((size & !4095) - 8192, "1.0"));
|
||||||
|
}
|
||||||
|
for (off, ver) in cands {
|
||||||
|
let Ok(b) = read_vec(src, off, 4096) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if b.len() < 4096 || le32(&b, 0) != 0xA92B_4EFC || le32(&b, 4) != 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let level = level_from(le32(&b, 72) as i32)?;
|
||||||
|
let max_dev = le32(&b, 220) as usize;
|
||||||
|
let dev_number = le32(&b, 160) as usize;
|
||||||
|
let role = if dev_number < max_dev.min(1024) && 256 + dev_number * 2 + 2 <= b.len() {
|
||||||
|
let r = u16::from_le_bytes([b[256 + dev_number * 2], b[256 + dev_number * 2 + 1]]);
|
||||||
|
(r < 0xFFFE).then_some(r as u32)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let mut uuid = [0u8; 16];
|
||||||
|
uuid.copy_from_slice(&b[16..32]);
|
||||||
|
let name = String::from_utf8_lossy(&b[32..64])
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.to_string();
|
||||||
|
let data_offset = le64(&b, 128) * 512;
|
||||||
|
let data_size = le64(&b, 136) * 512;
|
||||||
|
let raid_disks = le32(&b, 92);
|
||||||
|
let chunk = le32(&b, 88) as u64 * 512;
|
||||||
|
let used = le64(&b, 80) * 512;
|
||||||
|
let array_size = match level {
|
||||||
|
Level::R0 => used * raid_disks as u64,
|
||||||
|
Level::R5 | Level::R4 => used * (raid_disks as u64).saturating_sub(1),
|
||||||
|
Level::R6 => used * (raid_disks as u64).saturating_sub(2),
|
||||||
|
_ => used,
|
||||||
|
};
|
||||||
|
return Some(MdSuper {
|
||||||
|
version: ver.into(),
|
||||||
|
uuid,
|
||||||
|
name,
|
||||||
|
level,
|
||||||
|
layout: le32(&b, 76),
|
||||||
|
chunk,
|
||||||
|
raid_disks,
|
||||||
|
role,
|
||||||
|
data_offset,
|
||||||
|
data_size,
|
||||||
|
events: le64(&b, 200),
|
||||||
|
array_size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 0.90: no fim, alinhado a 64 KiB.
|
||||||
|
if size > 131072 {
|
||||||
|
let off = (size & !65535) - 65536;
|
||||||
|
if let Ok(b) = read_vec(src, off, 4096) {
|
||||||
|
if b.len() >= 1024 && le32(&b, 0) == 0xA92B_4EFC && le32(&b, 4) == 0 {
|
||||||
|
let level = level_from(le32(&b, 28) as i32)?;
|
||||||
|
let mut uuid = [0u8; 16];
|
||||||
|
uuid[0..4].copy_from_slice(&b[20..24]);
|
||||||
|
uuid[4..16].copy_from_slice(&b[52..64]);
|
||||||
|
let role = {
|
||||||
|
let r = le32(&b, 768 + 12);
|
||||||
|
(r < 0xFFFE).then_some(r)
|
||||||
|
};
|
||||||
|
let raid_disks = le32(&b, 40);
|
||||||
|
let used = le32(&b, 32) as u64 * 1024;
|
||||||
|
return Some(MdSuper {
|
||||||
|
version: "0.90".into(),
|
||||||
|
uuid,
|
||||||
|
name: String::new(),
|
||||||
|
level,
|
||||||
|
layout: le32(&b, 256),
|
||||||
|
chunk: le32(&b, 260) as u64,
|
||||||
|
raid_disks,
|
||||||
|
role,
|
||||||
|
data_offset: 0,
|
||||||
|
data_size: used,
|
||||||
|
events: ((le32(&b, 39 * 4) as u64) << 32) | le32(&b, 40 * 4) as u64,
|
||||||
|
array_size: match level {
|
||||||
|
Level::R0 => used * raid_disks as u64,
|
||||||
|
Level::R5 | Level::R4 => used * (raid_disks as u64).saturating_sub(1),
|
||||||
|
Level::R6 => used * (raid_disks as u64).saturating_sub(2),
|
||||||
|
_ => used,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uuid_str(u: &[u8; 16]) -> String {
|
||||||
|
u.iter().map(|b| format!("{b:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Um array reconhecido pelos superblocos dos discos fornecidos.
|
||||||
|
pub struct FoundArray {
|
||||||
|
pub uuid: String,
|
||||||
|
pub name: String,
|
||||||
|
pub info: MdSuper,
|
||||||
|
pub source: Arc<RaidSource>,
|
||||||
|
pub missing: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Agrupa membros por UUID e monta cada array (degradado se for possível).
|
||||||
|
pub fn assemble_md(members: Vec<(Arc<dyn Source>, MdSuper)>) -> Vec<Result<FoundArray, String>> {
|
||||||
|
let mut groups: Vec<(String, Vec<(Arc<dyn Source>, MdSuper)>)> = vec![];
|
||||||
|
for (s, m) in members {
|
||||||
|
let key = uuid_str(&m.uuid);
|
||||||
|
match groups.iter_mut().find(|g| g.0 == key) {
|
||||||
|
Some(g) => g.1.push((s, m)),
|
||||||
|
None => groups.push((key, vec![(s, m)])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut out = vec![];
|
||||||
|
for (uuid, mut list) in groups {
|
||||||
|
// O membro com mais eventos manda; membros desatualizados são tratados como ausentes.
|
||||||
|
list.sort_by_key(|(_, m)| std::cmp::Reverse(m.events));
|
||||||
|
let top = list[0].1.clone();
|
||||||
|
let n = top.raid_disks as usize;
|
||||||
|
let mut slots: Vec<Option<Member>> = vec![None; n];
|
||||||
|
let mut stale = 0;
|
||||||
|
for (s, m) in &list {
|
||||||
|
if let Some(r) = m.role {
|
||||||
|
if (r as usize) < n && slots[r as usize].is_none() {
|
||||||
|
if m.events + 1 < top.events && top.events > 0 {
|
||||||
|
stale += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
slots[r as usize] = Some(Member {
|
||||||
|
src: s.clone(),
|
||||||
|
data_offset: m.data_offset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let missing = slots.iter().filter(|s| s.is_none()).count();
|
||||||
|
let mut p = RaidParams::simple(
|
||||||
|
top.level,
|
||||||
|
top.chunk.max(512),
|
||||||
|
Layout::from_md(top.layout),
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
if top.level == Level::R10 {
|
||||||
|
p.near = top.layout & 0xff;
|
||||||
|
p.far = (top.layout >> 8) & 0xff;
|
||||||
|
p.far_offset = top.layout & 0x10000 != 0;
|
||||||
|
}
|
||||||
|
if top.data_size > 0 {
|
||||||
|
p.per_member = top.data_size;
|
||||||
|
}
|
||||||
|
match RaidSource::new(p) {
|
||||||
|
Ok(src) => out.push(Ok(FoundArray {
|
||||||
|
uuid: uuid.clone(),
|
||||||
|
name: top.name.clone(),
|
||||||
|
info: top,
|
||||||
|
source: Arc::new(src),
|
||||||
|
missing: missing + stale,
|
||||||
|
})),
|
||||||
|
Err(e) => out.push(Err(format!("array {uuid}: {e}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
//! Descobre sozinho nível, ordem dos discos, tamanho de faixa e layout de um RAID sem
|
||||||
|
//! metadados, testando as combinações e medindo a coerência do sistema de arquivos que sai.
|
||||||
|
|
||||||
|
use crate::partition;
|
||||||
|
use crate::raid::{Layout, Level, Member, RaidParams, RaidSource};
|
||||||
|
use crate::source::{read_vec, MemSource, Source};
|
||||||
|
use crate::types::FsKind;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Candidate {
|
||||||
|
pub level: Level,
|
||||||
|
pub chunk: u64,
|
||||||
|
pub layout: Layout,
|
||||||
|
/// Índices dos membros fornecidos, na ordem do array.
|
||||||
|
pub order: Vec<usize>,
|
||||||
|
pub score: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Candidate {
|
||||||
|
pub fn params(&self, members: &[Arc<dyn Source>]) -> RaidParams {
|
||||||
|
RaidParams::simple(
|
||||||
|
self.level,
|
||||||
|
self.chunk,
|
||||||
|
self.layout,
|
||||||
|
self.order
|
||||||
|
.iter()
|
||||||
|
.map(|&i| {
|
||||||
|
Some(Member {
|
||||||
|
src: members[i].clone(),
|
||||||
|
data_offset: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFIX: u64 = 24 << 20;
|
||||||
|
const CHUNKS: [u64; 9] = [
|
||||||
|
4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576,
|
||||||
|
];
|
||||||
|
|
||||||
|
fn permutations(n: usize) -> Vec<Vec<usize>> {
|
||||||
|
fn rec(cur: &mut Vec<usize>, used: &mut Vec<bool>, n: usize, out: &mut Vec<Vec<usize>>) {
|
||||||
|
if cur.len() == n {
|
||||||
|
out.push(cur.clone());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for i in 0..n {
|
||||||
|
if !used[i] {
|
||||||
|
used[i] = true;
|
||||||
|
cur.push(i);
|
||||||
|
rec(cur, used, n, out);
|
||||||
|
cur.pop();
|
||||||
|
used[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut out = vec![];
|
||||||
|
rec(&mut vec![], &mut vec![false; n], n, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ntfs_score(src: &dyn Source, start: u64) -> f64 {
|
||||||
|
let Ok(b) = read_vec(src, start, 512) else {
|
||||||
|
return 0.0;
|
||||||
|
};
|
||||||
|
if b.len() < 512 || &b[3..11] != b"NTFS " {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let bps = u16::from_le_bytes([b[11], b[12]]) as u64;
|
||||||
|
let spc = b[13] as u64;
|
||||||
|
let mft = u64::from_le_bytes(b[0x30..0x38].try_into().unwrap());
|
||||||
|
let rs = b[0x40] as i8;
|
||||||
|
let rec = if rs > 0 {
|
||||||
|
rs as u64 * bps * spc
|
||||||
|
} else {
|
||||||
|
1u64 << (-rs)
|
||||||
|
};
|
||||||
|
if bps == 0 || spc == 0 || rec == 0 || rec > 65536 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let mft_off = start + mft * bps * spc;
|
||||||
|
let (mut ok, mut total) = (0, 0);
|
||||||
|
for k in [
|
||||||
|
0u64, 1, 2, 3, 5, 11, 16, 24, 40, 100, 250, 600, 1500, 3000, 6000, 12000, 24000, 50000,
|
||||||
|
] {
|
||||||
|
let off = mft_off + k * rec;
|
||||||
|
if off + rec > src.size() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let Ok(r) = read_vec(src, off, 64) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if r.len() < 64 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Registros além do fim do MFT estão zerados: não contam contra.
|
||||||
|
if r.iter().all(|&x| x == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
total += 1;
|
||||||
|
if &r[..4] == b"FILE" && u32::from_le_bytes(r[0x2C..0x30].try_into().unwrap()) as u64 == k {
|
||||||
|
ok += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
1.0 + 10.0 * ok as f64 / total as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coerência do volume que começa em `start`.
|
||||||
|
fn volume_score(src: &dyn Source, start: u64, fs: FsKind) -> f64 {
|
||||||
|
match fs {
|
||||||
|
FsKind::Ntfs => ntfs_score(src, start),
|
||||||
|
FsKind::Ext => crate::fs::ext::consistency_score(src, start).unwrap_or(0.5),
|
||||||
|
FsKind::Unknown => 0.0,
|
||||||
|
_ => 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fs_score(src: &dyn Source) -> f64 {
|
||||||
|
let layout = partition::analyze(src, false);
|
||||||
|
let mut best = 0.0f64;
|
||||||
|
for p in &layout.partitions {
|
||||||
|
best = best.max(
|
||||||
|
volume_score(src, p.start, p.fs) + if p.fs != FsKind::Unknown { 0.5 } else { 0.0 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_sector_plausible(src: &dyn Source) -> bool {
|
||||||
|
let Ok(b) = read_vec(src, 0, 4096) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
b.len() >= 1100 && ((b[510] == 0x55 && b[511] == 0xAA) || partition::detect_fs_in(&b).is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Testa as combinações e devolve as melhores (maior pontuação primeiro).
|
||||||
|
pub fn detect(
|
||||||
|
members: &[Arc<dyn Source>],
|
||||||
|
cancelled: &(dyn Fn() -> bool + Sync),
|
||||||
|
) -> Vec<Candidate> {
|
||||||
|
let n = members.len();
|
||||||
|
if n < 2 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
// Trabalha sobre um prefixo em memória de cada disco.
|
||||||
|
let cached: Vec<Arc<dyn Source>> = members
|
||||||
|
.iter()
|
||||||
|
.map(|m| {
|
||||||
|
let len = m.size().min(PREFIX) as usize;
|
||||||
|
Arc::new(MemSource(read_vec(m.as_ref(), 0, len).unwrap_or_default())) as Arc<dyn Source>
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let min_len = cached.iter().map(|c| c.size()).min().unwrap_or(0);
|
||||||
|
if min_len < 1 << 20 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut out: Vec<Candidate> = vec![];
|
||||||
|
|
||||||
|
// RAID 1: cópias idênticas.
|
||||||
|
let probe = |s: &Arc<dyn Source>| read_vec(s.as_ref(), 0, 1 << 20).unwrap_or_default();
|
||||||
|
let first = probe(&cached[0]);
|
||||||
|
if first.iter().any(|&x| x != 0) && cached.iter().skip(1).all(|c| probe(c) == first) {
|
||||||
|
let c = Candidate {
|
||||||
|
level: Level::R1,
|
||||||
|
chunk: 65536,
|
||||||
|
layout: Layout::LeftSymmetric,
|
||||||
|
order: (0..n).collect(),
|
||||||
|
score: fs_score(cached[0].as_ref()) + 1.0,
|
||||||
|
};
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAID 5/4 satisfaz "XOR de todos os discos = 0" em toda faixa.
|
||||||
|
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 mut xor_zero = 0usize;
|
||||||
|
let mut nonzero_rows = 0usize;
|
||||||
|
for blk in 0..sample / 4096 {
|
||||||
|
let mut any = false;
|
||||||
|
let mut z = true;
|
||||||
|
for i in 0..4096 {
|
||||||
|
let mut x = 0u8;
|
||||||
|
for b in &bufs {
|
||||||
|
x ^= b[blk * 4096 + i];
|
||||||
|
any |= b[blk * 4096 + i] != 0;
|
||||||
|
}
|
||||||
|
if x != 0 {
|
||||||
|
z = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if any {
|
||||||
|
nonzero_rows += 1;
|
||||||
|
if z {
|
||||||
|
xor_zero += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let parity5 = nonzero_rows > 8 && xor_zero * 100 >= nonzero_rows * 95;
|
||||||
|
|
||||||
|
let mut levels: Vec<Level> = vec![];
|
||||||
|
if parity5 && n >= 3 {
|
||||||
|
levels.push(Level::R5);
|
||||||
|
} else {
|
||||||
|
levels.push(Level::R0);
|
||||||
|
if n >= 4 {
|
||||||
|
levels.push(Level::R6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 perms = permutations(n);
|
||||||
|
for level in levels {
|
||||||
|
let layouts: Vec<Layout> = if matches!(level, Level::R0) {
|
||||||
|
vec![Layout::LeftSymmetric]
|
||||||
|
} else {
|
||||||
|
Layout::all_raid5().to_vec()
|
||||||
|
};
|
||||||
|
for &chunk in &CHUNKS {
|
||||||
|
for layout in &layouts {
|
||||||
|
for order in &perms {
|
||||||
|
if cancelled() {
|
||||||
|
return finish(out);
|
||||||
|
}
|
||||||
|
let cand = Candidate {
|
||||||
|
level,
|
||||||
|
chunk,
|
||||||
|
layout: *layout,
|
||||||
|
order: order.clone(),
|
||||||
|
score: 0.0,
|
||||||
|
};
|
||||||
|
let params = cand.params(&cached);
|
||||||
|
let Ok(r) = RaidSource::new(params) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// O primeiro setor do volume precisa estar num disco que o tenha.
|
||||||
|
if any_first {
|
||||||
|
let mut b = [0u8; 512];
|
||||||
|
let _ = r.read_at(0, &mut b);
|
||||||
|
let m0 = order.iter().position(|&i| {
|
||||||
|
let mut c = [0u8; 512];
|
||||||
|
let _ = cached[i].read_at(0, &mut c);
|
||||||
|
c == b
|
||||||
|
});
|
||||||
|
match m0 {
|
||||||
|
Some(p) if ok_first[order[p]] => {}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let score = fs_score(&r);
|
||||||
|
if score > 0.0 {
|
||||||
|
out.push(Candidate { score, ..cand });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finish(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.truncate(10);
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -375,6 +375,44 @@ pub fn write_file(src: &dyn Source, f: &Found, path: &Path) -> Result<Hashes> {
|
|||||||
out.write_all(bytes)?;
|
out.write_all(bytes)?;
|
||||||
hasher.update(bytes);
|
hasher.update(bytes);
|
||||||
}
|
}
|
||||||
|
DataLoc::Parts { parts } => {
|
||||||
|
for part in parts {
|
||||||
|
match part {
|
||||||
|
Part::Bytes { bytes } => {
|
||||||
|
out.write_all(bytes)?;
|
||||||
|
hasher.update(bytes);
|
||||||
|
}
|
||||||
|
Part::Zeros { len } => {
|
||||||
|
let z = vec![0u8; (1 << 20).min(*len as usize).max(1)];
|
||||||
|
let mut left = *len;
|
||||||
|
while left > 0 {
|
||||||
|
let n = left.min(z.len() as u64) as usize;
|
||||||
|
out.write_all(&z[..n])?;
|
||||||
|
hasher.update(&z[..n]);
|
||||||
|
left -= n as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Part::Packed(pk) => {
|
||||||
|
let v = pk.read(src)?;
|
||||||
|
out.write_all(&v)?;
|
||||||
|
hasher.update(&v);
|
||||||
|
}
|
||||||
|
Part::Run { offset, len } => {
|
||||||
|
let mut done = 0u64;
|
||||||
|
while done < *len {
|
||||||
|
let n = (*len - done).min(1 << 20) as usize;
|
||||||
|
let got = read_vec(src, offset + done, n)?;
|
||||||
|
if got.len() < n {
|
||||||
|
return Err(Error::msg("leitura além do fim da origem"));
|
||||||
|
}
|
||||||
|
out.write_all(&got)?;
|
||||||
|
hasher.update(&got);
|
||||||
|
done += n as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
DataLoc::Extents { extents } => {
|
DataLoc::Extents { extents } => {
|
||||||
let mut left = f.size;
|
let mut left = f.size;
|
||||||
let mut buf = vec![0u8; 1 << 20];
|
let mut buf = vec![0u8; 1 << 20];
|
||||||
@@ -425,6 +463,34 @@ pub fn read_found_at(
|
|||||||
buf[..n].copy_from_slice(&bytes[o..o + n]);
|
buf[..n].copy_from_slice(&bytes[o..o + n]);
|
||||||
Ok(n)
|
Ok(n)
|
||||||
}
|
}
|
||||||
|
DataLoc::Parts { parts } => {
|
||||||
|
let mut pos = 0u64;
|
||||||
|
for p in parts {
|
||||||
|
let plen = p.len();
|
||||||
|
if offset < pos + plen {
|
||||||
|
let within = offset - pos;
|
||||||
|
let n = want.min((plen - within) as usize);
|
||||||
|
return match p {
|
||||||
|
Part::Bytes { bytes } => {
|
||||||
|
buf[..n].copy_from_slice(&bytes[within as usize..within as usize + n]);
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
Part::Run { offset: o, .. } => src.read_at(o + within, &mut buf[..n]),
|
||||||
|
Part::Zeros { .. } => {
|
||||||
|
buf[..n].fill(0);
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
Part::Packed(pk) => {
|
||||||
|
let v = pk.read(src)?;
|
||||||
|
buf[..n].copy_from_slice(&v[within as usize..within as usize + n]);
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pos += plen;
|
||||||
|
}
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
DataLoc::Extents { extents } => {
|
DataLoc::Extents { extents } => {
|
||||||
let mut pos = 0u64;
|
let mut pos = 0u64;
|
||||||
for e in extents {
|
for e in extents {
|
||||||
|
|||||||
@@ -292,12 +292,7 @@ footer{margin-top:34px;color:#5b6f6d;font-size:12px}@media print{body{background
|
|||||||
human(f.size),
|
human(f.size),
|
||||||
iso(f.modified),
|
iso(f.modified),
|
||||||
iso(f.created),
|
iso(f.created),
|
||||||
match f.origin {
|
f.origin.label(),
|
||||||
Origin::Carved => "assinatura",
|
|
||||||
Origin::Ntfs => "NTFS",
|
|
||||||
Origin::Fat => "FAT",
|
|
||||||
Origin::Exfat => "exFAT",
|
|
||||||
},
|
|
||||||
f.first_offset().map(|o| o.to_string()).unwrap_or_default()
|
f.first_offset().map(|o| o.to_string()).unwrap_or_default()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ pub trait Sink {
|
|||||||
fn found(&mut self, f: Found);
|
fn found(&mut self, f: Found);
|
||||||
/// Um achado de carving confirmou um arquivo já listado por metadados.
|
/// Um achado de carving confirmou um arquivo já listado por metadados.
|
||||||
fn confirm(&mut self, id: u64, confidence: Confidence);
|
fn confirm(&mut self, id: u64, confidence: Confidence);
|
||||||
|
/// Um achado já emitido ganhou dados melhores (ex.: JPEG remontado de fragmentos).
|
||||||
|
fn replace(&mut self, f: Found) {
|
||||||
|
self.found(f);
|
||||||
|
}
|
||||||
fn progress(&mut self, p: Progress);
|
fn progress(&mut self, p: Progress);
|
||||||
fn warning(&mut self, w: String);
|
fn warning(&mut self, w: String);
|
||||||
fn layout(&mut self, l: &DiskLayout);
|
fn layout(&mut self, l: &DiskLayout);
|
||||||
@@ -83,6 +87,20 @@ pub fn run(
|
|||||||
l
|
l
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let mut layout = layout;
|
||||||
|
if opts.skip_primary {
|
||||||
|
layout.partitions.clear();
|
||||||
|
layout.gaps.clear();
|
||||||
|
layout.table = "membro de RAID".into();
|
||||||
|
}
|
||||||
|
// Volumes lógicos e outros volumes montados que não estão na tabela do disco.
|
||||||
|
for p in &opts.extra_parts {
|
||||||
|
if !layout.partitions.iter().any(|q| q.start == p.start) {
|
||||||
|
let mut p = p.clone();
|
||||||
|
p.index = layout.partitions.len();
|
||||||
|
layout.partitions.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
cp.layout = Some(layout.clone());
|
cp.layout = Some(layout.clone());
|
||||||
|
|
||||||
// Índices para fusão: primeiro offset -> (id, confiança), e extents de achados por metadados.
|
// Índices para fusão: primeiro offset -> (id, confiança), e extents de achados por metadados.
|
||||||
@@ -128,6 +146,10 @@ pub fn run(
|
|||||||
}
|
}
|
||||||
FsKind::Exfat => Some(fs::exfat::scan(&mut ctx, p.start)),
|
FsKind::Exfat => Some(fs::exfat::scan(&mut ctx, p.start)),
|
||||||
FsKind::Ext => Some(fs::ext::scan(&mut ctx, p.start, p.len)),
|
FsKind::Ext => Some(fs::ext::scan(&mut ctx, p.start, p.len)),
|
||||||
|
FsKind::HfsPlus => Some(fs::hfsplus::scan(&mut ctx, p.start, p.len)),
|
||||||
|
FsKind::Btrfs => Some(fs::btrfs::scan(&mut ctx, p.start, p.len)),
|
||||||
|
FsKind::Xfs => Some(fs::xfs::scan(&mut ctx, p.start, p.len)),
|
||||||
|
FsKind::Apfs => Some(fs::apfs::scan(&mut ctx, p.start, p.len)),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let _ = progress_phase;
|
let _ = progress_phase;
|
||||||
@@ -179,12 +201,25 @@ pub fn run(
|
|||||||
categories: &opts.categories,
|
categories: &opts.categories,
|
||||||
};
|
};
|
||||||
let mut handler = Handler {
|
let mut handler = Handler {
|
||||||
|
src,
|
||||||
sink,
|
sink,
|
||||||
starts,
|
starts,
|
||||||
fs_extents,
|
fs_extents,
|
||||||
next_id: &next_id,
|
next_id: &next_id,
|
||||||
done,
|
done,
|
||||||
total,
|
total,
|
||||||
|
index: crate::defrag::ClusterIndex::new(src.size()),
|
||||||
|
pending: vec![],
|
||||||
|
intact_mp4: vec![],
|
||||||
|
refs: opts
|
||||||
|
.references
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| {
|
||||||
|
let s = crate::source::open_path(std::path::Path::new(p)).ok()?;
|
||||||
|
crate::mp4rebuild::config_from_mp4(s.as_ref(), 0, s.size())
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
carved: vec![],
|
||||||
};
|
};
|
||||||
for ri in 0..cp.ranges.len() {
|
for ri in 0..cp.ranges.len() {
|
||||||
let (start, end) = (cp.ranges[ri].start, cp.ranges[ri].end);
|
let (start, end) = (cp.ranges[ri].start, cp.ranges[ri].end);
|
||||||
@@ -200,19 +235,118 @@ pub fn run(
|
|||||||
return Outcome::Interrupted(cp);
|
return Outcome::Interrupted(cp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
handler.rebuild_pending();
|
||||||
Outcome::Completed
|
Outcome::Completed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Arquivo que parece quebrado: tenta recompor depois de varrer o disco todo.
|
||||||
|
struct Pending {
|
||||||
|
found: Found,
|
||||||
|
hit_len: u64,
|
||||||
|
mp4: bool,
|
||||||
|
}
|
||||||
|
|
||||||
struct Handler<'a> {
|
struct Handler<'a> {
|
||||||
|
src: &'a dyn Source,
|
||||||
sink: &'a mut dyn Sink,
|
sink: &'a mut dyn Sink,
|
||||||
starts: HashMap<u64, (u64, Confidence)>,
|
starts: HashMap<u64, (u64, Confidence)>,
|
||||||
fs_extents: Vec<Extent>,
|
fs_extents: Vec<Extent>,
|
||||||
next_id: &'a AtomicU64,
|
next_id: &'a AtomicU64,
|
||||||
done: u64,
|
done: u64,
|
||||||
total: u64,
|
total: u64,
|
||||||
|
index: crate::defrag::ClusterIndex,
|
||||||
|
pending: Vec<Pending>,
|
||||||
|
/// Vídeos íntegros achados no carving (início, tamanho): fontes de SPS/PPS.
|
||||||
|
intact_mp4: Vec<(u64, u64)>,
|
||||||
|
refs: Vec<crate::mp4rebuild::MediaConfig>,
|
||||||
|
/// Extents de arquivos carved já emitidos (não podem servir de fragmento para outro).
|
||||||
|
carved: Vec<Extent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<'_> {
|
||||||
|
fn rebuild_mp4(&mut self, p: Pending, off: u64) {
|
||||||
|
// Referências: as informadas pelo usuário e os vídeos íntegros mais próximos no disco.
|
||||||
|
let mut refs = self.refs.clone();
|
||||||
|
let mut near = self.intact_mp4.clone();
|
||||||
|
near.sort_by_key(|&(o, _)| o.abs_diff(off));
|
||||||
|
for (o, l) in near.into_iter().take(6) {
|
||||||
|
if let Some(c) = crate::mp4rebuild::config_from_mp4(self.src, o, l) {
|
||||||
|
refs.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(r) = crate::mp4rebuild::rebuild(self.src, off, p.hit_len, &refs) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(pre) = crate::source::read_vec(self.src, off, r.pre_len as usize) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let payload_len = r.payload.1 - r.payload.0;
|
||||||
|
let mut f = p.found;
|
||||||
|
let hdr = r.mdat_header();
|
||||||
|
f.size = pre.len() as u64 + hdr.len() as u64 + payload_len + r.moov.len() as u64;
|
||||||
|
f.data = DataLoc::Parts {
|
||||||
|
parts: vec![
|
||||||
|
Part::Bytes { bytes: pre },
|
||||||
|
Part::Bytes { bytes: hdr },
|
||||||
|
Part::Run {
|
||||||
|
offset: r.payload.0,
|
||||||
|
len: payload_len,
|
||||||
|
},
|
||||||
|
Part::Bytes { bytes: r.moov },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
f.ext = "mp4".into();
|
||||||
|
f.name = format!("arquivo_{:06}.mp4", f.id);
|
||||||
|
f.confidence = Confidence::Medium;
|
||||||
|
f.note = Some(format!(
|
||||||
|
"índice (moov) reconstruído: {} quadros {}x{} a {:.1} fps, sem áudio; ajuste de tempo aproximado",
|
||||||
|
r.frames, r.width, r.height, r.fps
|
||||||
|
));
|
||||||
|
self.carved.push(Extent::new(r.payload.0, payload_len));
|
||||||
|
self.sink.replace(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_pending(&mut self) {
|
||||||
|
let pend = std::mem::take(&mut self.pending);
|
||||||
|
for p in pend {
|
||||||
|
let off = p.found.first_offset().unwrap_or(0);
|
||||||
|
if p.mp4 {
|
||||||
|
self.rebuild_mp4(p, off);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut used = self.fs_extents.clone();
|
||||||
|
used.extend(self.carved.iter().copied());
|
||||||
|
// O próprio começo do arquivo não conta como "ocupado" para ele mesmo.
|
||||||
|
used.retain(|e| !(e.offset == off));
|
||||||
|
let pool = crate::defrag::Pool {
|
||||||
|
index: &self.index,
|
||||||
|
used: &used,
|
||||||
|
};
|
||||||
|
if let crate::defrag::Analysis::Rebuilt { extents, size } =
|
||||||
|
crate::defrag::analyze_jpeg(self.src, off, p.hit_len, &pool, true)
|
||||||
|
{
|
||||||
|
let mut f = p.found;
|
||||||
|
f.size = size;
|
||||||
|
f.data = DataLoc::Extents {
|
||||||
|
extents: extents.clone(),
|
||||||
|
};
|
||||||
|
f.confidence = Confidence::Medium;
|
||||||
|
f.note = Some(format!(
|
||||||
|
"remontado a partir de {} fragmentos separados no disco; confira a imagem",
|
||||||
|
extents.len()
|
||||||
|
));
|
||||||
|
self.carved.extend(extents);
|
||||||
|
self.sink.replace(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CarveEvents for Handler<'_> {
|
impl CarveEvents for Handler<'_> {
|
||||||
|
fn chunk(&mut self, abs: u64, data: &[u8]) {
|
||||||
|
self.index.scan_chunk(abs, data);
|
||||||
|
}
|
||||||
|
|
||||||
fn advance(&mut self, n: u64) {
|
fn advance(&mut self, n: u64) {
|
||||||
self.done += n;
|
self.done += n;
|
||||||
self.sink.progress(Progress {
|
self.sink.progress(Progress {
|
||||||
@@ -233,6 +367,38 @@ impl CarveEvents for Handler<'_> {
|
|||||||
if overlaps(&self.fs_extents, off, 1) {
|
if overlaps(&self.fs_extents, off, 1) {
|
||||||
return HitAction::Continue; // dentro de arquivo já listado (ex.: miniatura embutida)
|
return HitAction::Continue; // dentro de arquivo já listado (ex.: miniatura embutida)
|
||||||
}
|
}
|
||||||
|
let mut hit = hit.clone();
|
||||||
|
let mut skip_len = None;
|
||||||
|
let mut pending_len = None;
|
||||||
|
if hit.ext == "jpg" {
|
||||||
|
let pool = crate::defrag::Pool {
|
||||||
|
index: &self.index,
|
||||||
|
used: &[],
|
||||||
|
};
|
||||||
|
match crate::defrag::analyze_jpeg(self.src, off, hit.len, &pool, false) {
|
||||||
|
crate::defrag::Analysis::Intact { len } => {
|
||||||
|
hit.len = len;
|
||||||
|
hit.confidence = Confidence::High;
|
||||||
|
}
|
||||||
|
crate::defrag::Analysis::Broken { good_len } => {
|
||||||
|
pending_len = Some(hit.len);
|
||||||
|
hit.len = good_len;
|
||||||
|
hit.confidence = Confidence::Low;
|
||||||
|
skip_len = Some(good_len);
|
||||||
|
}
|
||||||
|
crate::defrag::Analysis::Unsupported | crate::defrag::Analysis::Rebuilt { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut pending_mp4 = false;
|
||||||
|
if matches!(hit.ext, "mp4" | "mov" | "m4v" | "3gp") {
|
||||||
|
if hit.confidence == Confidence::High {
|
||||||
|
self.intact_mp4.push((off, hit.len));
|
||||||
|
} else {
|
||||||
|
pending_mp4 = true;
|
||||||
|
pending_len = Some(hit.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let hit = &hit;
|
||||||
let id = self
|
let id = self
|
||||||
.next_id
|
.next_id
|
||||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
@@ -263,8 +429,25 @@ impl CarveEvents for Handler<'_> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
self.starts.insert(off, (id, hit.confidence));
|
self.starts.insert(off, (id, hit.confidence));
|
||||||
|
self.carved.push(Extent::new(off, hit.len));
|
||||||
|
if let Some(hl) = pending_len {
|
||||||
|
let mut pf = f.clone();
|
||||||
|
pf.note = Some(if pending_mp4 {
|
||||||
|
"vídeo sem índice (moov)".into()
|
||||||
|
} else {
|
||||||
|
"JPEG interrompido ou fragmentado".into()
|
||||||
|
});
|
||||||
|
self.pending.push(Pending {
|
||||||
|
found: pf,
|
||||||
|
hit_len: hl,
|
||||||
|
mp4: pending_mp4,
|
||||||
|
});
|
||||||
|
}
|
||||||
self.sink.found(f);
|
self.sink.found(f);
|
||||||
HitAction::Skip
|
match skip_len {
|
||||||
|
Some(n) => HitAction::SkipLen(n),
|
||||||
|
None => HitAction::Skip,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +472,7 @@ fn build_ranges(
|
|||||||
let free = merge_extents(free.to_vec());
|
let free = merge_extents(free.to_vec());
|
||||||
let mut out: Vec<Extent> = vec![];
|
let mut out: Vec<Extent> = vec![];
|
||||||
for p in &layout.partitions {
|
for p in &layout.partitions {
|
||||||
if p.encrypted.is_some() {
|
if p.encrypted.is_some() || opts.covered.contains(&p.start) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let whole = Extent::new(p.start, p.len.min(disk_size.saturating_sub(p.start)));
|
let whole = Extent::new(p.start, p.len.min(disk_size.saturating_sub(p.start)));
|
||||||
|
|||||||
@@ -137,6 +137,17 @@ impl Sink for JobSink {
|
|||||||
s.found[i].confidence = s.found[i].confidence.max(c);
|
s.found[i].confidence = s.found[i].confidence.max(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn replace(&mut self, f: Found) {
|
||||||
|
let mut s = self.0.st.lock().unwrap();
|
||||||
|
match s.index.get(&f.id).copied() {
|
||||||
|
Some(i) => s.found[i] = f,
|
||||||
|
None => {
|
||||||
|
let i = s.found.len();
|
||||||
|
s.index.insert(f.id, i);
|
||||||
|
s.found.push(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fn progress(&mut self, p: Progress) {
|
fn progress(&mut self, p: Progress) {
|
||||||
self.0.st.lock().unwrap().progress = p;
|
self.0.st.lock().unwrap().progress = p;
|
||||||
}
|
}
|
||||||
@@ -204,10 +215,38 @@ impl Engine {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- varredura
|
// ---------------------------------------------------------------- varredura
|
||||||
|
|
||||||
pub fn start_scan(&self, path: &str, opts: ScanOptions) -> Result<String> {
|
pub fn start_scan(&self, path: &str, mut opts: ScanOptions) -> Result<String> {
|
||||||
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
let raw = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
||||||
let fp = crate::source::fingerprint(src.as_ref());
|
let fp = crate::source::fingerprint(raw.as_ref());
|
||||||
let job = self.register(JobKind::Scan, path.to_string(), opts, Some(src));
|
let asm = crate::assemble::assemble_source(raw, &opts.assembly)?;
|
||||||
|
opts.extra_parts = asm.extra_parts.clone();
|
||||||
|
opts.covered = asm.covered.clone();
|
||||||
|
opts.skip_primary = asm.skip_primary;
|
||||||
|
if opts.assembly.remember_keys {
|
||||||
|
opts.assembly.keys = asm.keys.clone();
|
||||||
|
}
|
||||||
|
let job = self.register(
|
||||||
|
JobKind::Scan,
|
||||||
|
path.to_string(),
|
||||||
|
opts,
|
||||||
|
Some(asm.source.clone()),
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let mut s = job.st.lock().unwrap();
|
||||||
|
for u in &asm.unlocked {
|
||||||
|
s.warnings.push(format!(
|
||||||
|
"Volume cifrado aberto na posição {}: {}.",
|
||||||
|
u.start, u.description
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for l in &asm.locked {
|
||||||
|
s.warnings.push(format!(
|
||||||
|
"Volume cifrado na posição {} não foi aberto: {}.",
|
||||||
|
l.start, l.reason
|
||||||
|
));
|
||||||
|
}
|
||||||
|
s.warnings.extend(asm.notes.iter().cloned());
|
||||||
|
}
|
||||||
*job.fingerprint.lock().unwrap() = fp;
|
*job.fingerprint.lock().unwrap() = fp;
|
||||||
*job.session_file.lock().unwrap() = Some(new_session_path(path));
|
*job.session_file.lock().unwrap() = Some(new_session_path(path));
|
||||||
spawn_scan(job.clone(), None);
|
spawn_scan(job.clone(), None);
|
||||||
@@ -520,6 +559,17 @@ impl Engine {
|
|||||||
// ---------------------------------------------------------------- imagem
|
// ---------------------------------------------------------------- imagem
|
||||||
|
|
||||||
pub fn start_image(&self, path: &str, dest: &str) -> Result<String> {
|
pub fn start_image(&self, path: &str, dest: &str) -> Result<String> {
|
||||||
|
self.start_image_as(path, dest, &image::ImageFormat::Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
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(
|
||||||
Some(Path::new(path)),
|
Some(Path::new(path)),
|
||||||
@@ -542,7 +592,7 @@ impl Engine {
|
|||||||
total: t,
|
total: t,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
let res = image::create_image(src.as_ref(), &dest, &cancelled, &mut prog);
|
let res = image::create_image_as(src.as_ref(), &dest, &format, &cancelled, &mut prog);
|
||||||
let mut s = j2.st.lock().unwrap();
|
let mut s = j2.st.lock().unwrap();
|
||||||
match res {
|
match res {
|
||||||
Ok(rep) => {
|
Ok(rep) => {
|
||||||
@@ -735,6 +785,17 @@ impl Engine {
|
|||||||
"O arquivo {path} não é uma sessão do Guará válida (ou foi criado por uma versão incompatível): {e}"
|
"O arquivo {path} não é uma sessão do Guará válida (ou foi criado por uma versão incompatível): {e}"
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
// Clicar de novo numa sessão já aberta reaproveita o job em vez de criar outro igual.
|
||||||
|
let want = Path::new(path);
|
||||||
|
if let Some(j) = self
|
||||||
|
.jobs
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.values()
|
||||||
|
.find(|j| j.session_file.lock().unwrap().as_deref() == Some(want))
|
||||||
|
{
|
||||||
|
return Ok(j.id.clone());
|
||||||
|
}
|
||||||
let job = self.register(
|
let job = self.register(
|
||||||
JobKind::Scan,
|
JobKind::Scan,
|
||||||
data.target.clone(),
|
data.target.clone(),
|
||||||
@@ -803,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(), size, fp.as_deref()) => {
|
Ok(src)
|
||||||
*j.src.lock().unwrap() = Some(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);
|
||||||
}
|
}
|
||||||
@@ -815,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(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));
|
||||||
}
|
}
|
||||||
@@ -852,7 +919,7 @@ impl Engine {
|
|||||||
if fp.is_none() {
|
if fp.is_none() {
|
||||||
*j.fingerprint.lock().unwrap() = crate::source::fingerprint(src.as_ref());
|
*j.fingerprint.lock().unwrap() = crate::source::fingerprint(src.as_ref());
|
||||||
}
|
}
|
||||||
*j.src.lock().unwrap() = Some(src);
|
*j.src.lock().unwrap() = Some(finish_source(&j, src));
|
||||||
*j.src_path.lock().unwrap() = path.to_string();
|
*j.src_path.lock().unwrap() = path.to_string();
|
||||||
j.st.lock()
|
j.st.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -951,8 +1018,40 @@ impl Engine {
|
|||||||
"devices" => json!(device::list_devices()),
|
"devices" => json!(device::list_devices()),
|
||||||
"inspect" => {
|
"inspect" => {
|
||||||
let path = s("path")?;
|
let path = s("path")?;
|
||||||
let src = open_path(Path::new(&path)).map_err(|e| open_error(&path, e))?;
|
let asm: crate::assemble::Assembly = p
|
||||||
json!(partition::analyze(src.as_ref(), false))
|
.get("assembly")
|
||||||
|
.cloned()
|
||||||
|
.map(serde_json::from_value)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
let raw = open_path(Path::new(&path)).map_err(|e| open_error(&path, e))?;
|
||||||
|
let a = crate::assemble::assemble_source(raw, &asm)?;
|
||||||
|
let mut layout = partition::analyze(a.source.as_ref(), false);
|
||||||
|
if a.skip_primary {
|
||||||
|
layout.partitions.clear();
|
||||||
|
layout.gaps.clear();
|
||||||
|
layout.table = "membro de RAID".into();
|
||||||
|
}
|
||||||
|
for pi in &a.extra_parts {
|
||||||
|
if !layout.partitions.iter().any(|q| q.start == pi.start) {
|
||||||
|
let mut pi = pi.clone();
|
||||||
|
pi.index = layout.partitions.len();
|
||||||
|
layout.partitions.push(pi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout.warnings.extend(a.notes.iter().cloned());
|
||||||
|
for u in &a.unlocked {
|
||||||
|
layout
|
||||||
|
.warnings
|
||||||
|
.push(format!("Volume aberto em {}: {}.", u.start, u.description));
|
||||||
|
}
|
||||||
|
for l in &a.locked {
|
||||||
|
layout.warnings.push(format!(
|
||||||
|
"Volume cifrado em {} continua fechado: {}.",
|
||||||
|
l.start, l.reason
|
||||||
|
));
|
||||||
|
}
|
||||||
|
json!(layout)
|
||||||
}
|
}
|
||||||
"scan_start" => {
|
"scan_start" => {
|
||||||
let opts: ScanOptions = p
|
let opts: ScanOptions = p
|
||||||
@@ -1085,7 +1184,15 @@ impl Engine {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
json!({ "job": self.start_recover(&s("job")?, ids, filter, &s("dest")?, layout)? })
|
json!({ "job": self.start_recover(&s("job")?, ids, filter, &s("dest")?, layout)? })
|
||||||
}
|
}
|
||||||
"image_start" => json!({ "job": self.start_image(&s("path")?, &s("dest")?)? }),
|
"image_start" => {
|
||||||
|
let format: image::ImageFormat = p
|
||||||
|
.get("format")
|
||||||
|
.cloned()
|
||||||
|
.map(serde_json::from_value)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(image::ImageFormat::Raw);
|
||||||
|
json!({ "job": self.start_image_as(&s("path")?, &s("dest")?, &format)? })
|
||||||
|
}
|
||||||
"session_save" => {
|
"session_save" => {
|
||||||
self.save_session(&s("job")?, &s("path")?)?;
|
self.save_session(&s("job")?, &s("path")?)?;
|
||||||
json!({})
|
json!({})
|
||||||
@@ -1472,6 +1579,14 @@ impl Engine {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- sessões automáticas
|
// ---------------------------------------------------------------- sessões automáticas
|
||||||
|
|
||||||
|
/// Reaplica ao disco reaberto a montagem (volumes desbloqueados, RAID) da sessão.
|
||||||
|
fn finish_source(j: &Job, raw: Arc<dyn Source>) -> Arc<dyn Source> {
|
||||||
|
match crate::assemble::assemble_source(raw.clone(), &j.opts.assembly) {
|
||||||
|
Ok(a) => a.source,
|
||||||
|
Err(_) => raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sessions_dir() -> PathBuf {
|
fn sessions_dir() -> PathBuf {
|
||||||
crate::settings::config_dir().join("sessoes")
|
crate::settings::config_dir().join("sessoes")
|
||||||
}
|
}
|
||||||
@@ -1499,10 +1614,16 @@ fn new_session_path(target: &str) -> PathBuf {
|
|||||||
|
|
||||||
fn snapshot(j: &Job) -> SessionFile {
|
fn snapshot(j: &Job) -> SessionFile {
|
||||||
let s = j.st.lock().unwrap();
|
let s = j.st.lock().unwrap();
|
||||||
|
// Senhas e chaves nunca vão para o arquivo de sessão (só as chaves de volume, se o usuário pediu).
|
||||||
|
let mut options = j.opts.clone();
|
||||||
|
options.assembly.secrets.clear();
|
||||||
|
if !options.assembly.remember_keys {
|
||||||
|
options.assembly.keys.clear();
|
||||||
|
}
|
||||||
SessionFile {
|
SessionFile {
|
||||||
version: 1,
|
version: 1,
|
||||||
target: j.target.clone(),
|
target: j.target.clone(),
|
||||||
options: j.opts.clone(),
|
options,
|
||||||
found: s.found.clone(),
|
found: s.found.clone(),
|
||||||
checkpoint: s.checkpoint.clone(),
|
checkpoint: s.checkpoint.clone(),
|
||||||
layout: s.layout.clone(),
|
layout: s.layout.clone(),
|
||||||
|
|||||||
@@ -18,15 +18,21 @@ pub struct Hit {
|
|||||||
pub fn category_for_ext(ext: &str) -> Category {
|
pub fn category_for_ext(ext: &str) -> Category {
|
||||||
match ext.to_ascii_lowercase().as_str() {
|
match ext.to_ascii_lowercase().as_str() {
|
||||||
"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" => 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" => Category::Document,
|
| "txt" | "rtf" | "epub" | "md" | "csv" | "html" | "xml" | "json" | "swf" => {
|
||||||
"mp4" | "mov" | "m4v" | "avi" | "mkv" | "webm" | "wmv" | "flv" | "mpg" | "mpeg" | "3gp" => {
|
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" => Category::Audio,
|
|
||||||
"zip" | "rar" | "7z" | "gz" | "tar" | "bz2" | "xz" | "jar" | "apk" => Category::Archive,
|
|
||||||
"sqlite" | "db" | "sqlite3" | "mdb" | "accdb" => Category::Database,
|
|
||||||
_ => Category::Other,
|
_ => Category::Other,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,6 +66,21 @@ pub fn matches_ext(ext: &str, head: &[u8]) -> Option<bool> {
|
|||||||
"mkv" | "webm" => starts(&[0x1A, 0x45, 0xDF, 0xA3]),
|
"mkv" | "webm" => starts(&[0x1A, 0x45, 0xDF, 0xA3]),
|
||||||
"sqlite" | "sqlite3" | "db" => starts(b"SQLite format 3\0"),
|
"sqlite" | "sqlite3" | "db" => starts(b"SQLite format 3\0"),
|
||||||
"exe" | "dll" => starts(b"MZ"),
|
"exe" | "dll" => starts(b"MZ"),
|
||||||
|
"bz2" => starts(b"BZh"),
|
||||||
|
"zst" => starts(&[0x28, 0xB5, 0x2F, 0xFD]),
|
||||||
|
"cab" => starts(b"MSCF"),
|
||||||
|
"deb" => starts(b"!<arch>"),
|
||||||
|
"elf" => starts(&[0x7F, b'E', b'L', b'F']),
|
||||||
|
"tif" | "tiff" => starts(&[b'I', b'I', 42, 0]) || starts(&[b'M', b'M', 0, 42]),
|
||||||
|
"ico" => starts(&[0, 0, 1, 0]),
|
||||||
|
"rtf" => starts(b"{\\rtf"),
|
||||||
|
"swf" => starts(b"FWS") || starts(b"CWS"),
|
||||||
|
"flv" => starts(b"FLV"),
|
||||||
|
"pst" => starts(b"!BDN"),
|
||||||
|
"evtx" => starts(b"ElfFile"),
|
||||||
|
"mid" => starts(b"MThd"),
|
||||||
|
"pcap" => starts(&[0xD4, 0xC3, 0xB2, 0xA1]) || starts(&[0xA1, 0xB2, 0xC3, 0xD4]),
|
||||||
|
"wmv" | "wma" => starts(&[0x30, 0x26, 0xB2, 0x75]),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
Some(r)
|
Some(r)
|
||||||
@@ -68,23 +89,24 @@ pub fn matches_ext(ext: &str, head: &[u8]) -> Option<bool> {
|
|||||||
/// Tabela de primeiros bytes -> detectores candidatos.
|
/// Tabela de primeiros bytes -> detectores candidatos.
|
||||||
/// O carving consulta `candidates(first_byte)` e chama `detect`.
|
/// O carving consulta `candidates(first_byte)` e chama `detect`.
|
||||||
pub fn might_start(b0: u8) -> bool {
|
pub fn might_start(b0: u8) -> bool {
|
||||||
matches!(
|
crate::formats::might_start(b0)
|
||||||
b0,
|
|| matches!(
|
||||||
0xFF | 0x89
|
b0,
|
||||||
| b'G'
|
0xFF | 0x89
|
||||||
| b'B'
|
| b'G'
|
||||||
| b'%'
|
| b'B'
|
||||||
| b'P'
|
| b'%'
|
||||||
| 0xD0
|
| b'P'
|
||||||
| b'7'
|
| 0xD0
|
||||||
| b'S'
|
| b'7'
|
||||||
| b'R'
|
| b'S'
|
||||||
| b'I'
|
| b'R'
|
||||||
| b'M'
|
| b'I'
|
||||||
| 0x00
|
| b'M'
|
||||||
| b'f'
|
| 0x00
|
||||||
| 0x1F
|
| b'f'
|
||||||
)
|
| 0x1F
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_HEADER: usize = 64 * 1024;
|
const MAX_HEADER: usize = 64 * 1024;
|
||||||
@@ -135,7 +157,7 @@ pub fn detect(src: &dyn Source, off: u64, head: &[u8], max_size: u64) -> Option<
|
|||||||
if h.starts_with(b"MZ") {
|
if h.starts_with(b"MZ") {
|
||||||
return pe(src, off, max_size);
|
return pe(src, off, max_size);
|
||||||
}
|
}
|
||||||
None
|
crate::formats::detect(src, off, h, max_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- JPEG
|
// ---------------------------------------------------------------- JPEG
|
||||||
|
|||||||
@@ -272,7 +272,21 @@ pub fn open_path(path: &Path) -> io::Result<Arc<dyn Source>> {
|
|||||||
return Ok(Arc::new(open_windows_device(path)?));
|
return Ok(Arc::new(open_windows_device(path)?));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Arc::new(FileSource::open(path)?))
|
let file = FileSource::open(path)?;
|
||||||
|
// Imagens forenses EWF (E01/Ex01) são abertas como o disco que contêm.
|
||||||
|
let mut head = [0u8; 96];
|
||||||
|
let got = file.read_at(0, &mut head).unwrap_or(0);
|
||||||
|
if got >= 4 && head.starts_with(b"PK") && crate::aff4::is_aff4(&head[..got]) {
|
||||||
|
let a = crate::aff4::Aff4Source::open(path)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
|
||||||
|
return Ok(Arc::new(a));
|
||||||
|
}
|
||||||
|
if got >= 8 && crate::ewf::is_ewf_header(&head) {
|
||||||
|
let e = crate::ewf::EwfSource::open(path)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
|
||||||
|
return Ok(Arc::new(e));
|
||||||
|
}
|
||||||
|
Ok(Arc::new(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identidade do conteúdo de um disco: SHA-256 do tamanho + primeiro e último MiB
|
/// Identidade do conteúdo de um disco: SHA-256 do tamanho + primeiro e último MiB
|
||||||
|
|||||||
@@ -106,6 +106,149 @@ pub enum DataLoc {
|
|||||||
Extents {
|
Extents {
|
||||||
extents: Vec<Extent>,
|
extents: Vec<Extent>,
|
||||||
},
|
},
|
||||||
|
/// Arquivo montado: trechos em memória (cabeçalhos refeitos) e trechos do disco.
|
||||||
|
Parts {
|
||||||
|
parts: Vec<Part>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "part", rename_all = "lowercase")]
|
||||||
|
pub enum Part {
|
||||||
|
Bytes {
|
||||||
|
#[serde(with = "b64")]
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
},
|
||||||
|
/// Trecho contíguo da origem.
|
||||||
|
Run { offset: u64, len: u64 },
|
||||||
|
/// Zeros (buracos de arquivos esparsos).
|
||||||
|
Zeros { len: u64 },
|
||||||
|
/// Extensão comprimida na origem (Btrfs): descompactada ao ler.
|
||||||
|
Packed(Packed),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum PackedCodec {
|
||||||
|
Zlib,
|
||||||
|
Zstd,
|
||||||
|
Lzo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Packed {
|
||||||
|
/// Onde estão os bytes comprimidos na origem.
|
||||||
|
pub offset: u64,
|
||||||
|
pub disk_len: u64,
|
||||||
|
/// Tamanho da extensão depois de descompactada.
|
||||||
|
pub ram_len: u64,
|
||||||
|
/// Bytes a pular no começo da extensão descompactada.
|
||||||
|
pub skip: u64,
|
||||||
|
/// Bytes úteis a partir de `skip`.
|
||||||
|
pub len: u64,
|
||||||
|
pub codec: PackedCodec,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Packed {
|
||||||
|
/// Descompacta a extensão e devolve os `len` bytes úteis.
|
||||||
|
pub fn read(&self, src: &dyn crate::source::Source) -> std::io::Result<Vec<u8>> {
|
||||||
|
use std::io::Read;
|
||||||
|
let raw = crate::source::read_vec(src, self.offset, self.disk_len as usize)?;
|
||||||
|
let bad = |m: &str| {
|
||||||
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
format!("extensão comprimida ilegível ({m})"),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut data = match self.codec {
|
||||||
|
PackedCodec::Zlib => {
|
||||||
|
let mut v = Vec::with_capacity(self.ram_len as usize);
|
||||||
|
flate2::read::ZlibDecoder::new(&raw[..])
|
||||||
|
.read_to_end(&mut v)
|
||||||
|
.map_err(|_| bad("zlib"))?;
|
||||||
|
v
|
||||||
|
}
|
||||||
|
PackedCodec::Zstd => {
|
||||||
|
let mut v = Vec::with_capacity(self.ram_len as usize);
|
||||||
|
ruzstd::StreamingDecoder::new(&raw[..])
|
||||||
|
.map_err(|_| bad("zstd"))?
|
||||||
|
.read_to_end(&mut v)
|
||||||
|
.map_err(|_| bad("zstd"))?;
|
||||||
|
v
|
||||||
|
}
|
||||||
|
PackedCodec::Lzo => crate::fs::btrfs::lzo_btrfs(&raw, self.ram_len as usize)
|
||||||
|
.ok_or_else(|| bad("lzo"))?,
|
||||||
|
};
|
||||||
|
data.resize(self.ram_len.max(data.len() as u64) as usize, 0);
|
||||||
|
let s = (self.skip as usize).min(data.len());
|
||||||
|
let e = (s + self.len as usize).min(data.len());
|
||||||
|
let mut v = data[s..e].to_vec();
|
||||||
|
v.resize(self.len as usize, 0);
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Part {
|
||||||
|
pub fn len(&self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Part::Bytes { bytes } => bytes.len() as u64,
|
||||||
|
Part::Run { len, .. } | Part::Zeros { len } => *len,
|
||||||
|
Part::Packed(p) => p.len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod b64 {
|
||||||
|
use serde::{Deserialize, Deserializer, Serializer};
|
||||||
|
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> {
|
||||||
|
let mut out = String::with_capacity(b.len().div_ceil(3) * 4);
|
||||||
|
for c in b.chunks(3) {
|
||||||
|
let n = (c[0] as u32) << 16
|
||||||
|
| (*c.get(1).unwrap_or(&0) as u32) << 8
|
||||||
|
| *c.get(2).unwrap_or(&0) as u32;
|
||||||
|
out.push(T[(n >> 18) as usize & 63] as char);
|
||||||
|
out.push(T[(n >> 12) as usize & 63] as char);
|
||||||
|
out.push(if c.len() > 1 {
|
||||||
|
T[(n >> 6) as usize & 63] as char
|
||||||
|
} else {
|
||||||
|
'='
|
||||||
|
});
|
||||||
|
out.push(if c.len() > 2 {
|
||||||
|
T[n as usize & 63] as char
|
||||||
|
} else {
|
||||||
|
'='
|
||||||
|
});
|
||||||
|
}
|
||||||
|
s.serialize_str(&out)
|
||||||
|
}
|
||||||
|
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
|
||||||
|
let s = String::deserialize(d)?;
|
||||||
|
let mut out = vec![];
|
||||||
|
let (mut acc, mut bits) = (0u32, 0);
|
||||||
|
for c in s.bytes() {
|
||||||
|
let v = match c {
|
||||||
|
b'A'..=b'Z' => c - b'A',
|
||||||
|
b'a'..=b'z' => c - b'a' + 26,
|
||||||
|
b'0'..=b'9' => c - b'0' + 52,
|
||||||
|
b'+' => 62,
|
||||||
|
b'/' => 63,
|
||||||
|
_ => continue,
|
||||||
|
} as u32;
|
||||||
|
acc = (acc << 6) | v;
|
||||||
|
bits += 6;
|
||||||
|
if bits >= 8 {
|
||||||
|
bits -= 8;
|
||||||
|
out.push((acc >> bits) as u8);
|
||||||
|
acc &= (1 << bits) - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -114,9 +257,32 @@ pub enum Origin {
|
|||||||
Ntfs,
|
Ntfs,
|
||||||
Fat,
|
Fat,
|
||||||
Exfat,
|
Exfat,
|
||||||
|
Ext,
|
||||||
|
Btrfs,
|
||||||
|
Xfs,
|
||||||
|
Hfs,
|
||||||
|
Apfs,
|
||||||
|
Refs,
|
||||||
Carved,
|
Carved,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Origin {
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Origin::Carved => "assinatura",
|
||||||
|
Origin::Ntfs => "NTFS",
|
||||||
|
Origin::Fat => "FAT",
|
||||||
|
Origin::Exfat => "exFAT",
|
||||||
|
Origin::Ext => "ext4",
|
||||||
|
Origin::Btrfs => "Btrfs",
|
||||||
|
Origin::Xfs => "XFS",
|
||||||
|
Origin::Hfs => "HFS+",
|
||||||
|
Origin::Apfs => "APFS",
|
||||||
|
Origin::Refs => "ReFS",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Um arquivo encontrado (apagado, perdido ou reconstruído por assinatura).
|
/// Um arquivo encontrado (apagado, perdido ou reconstruído por assinatura).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Found {
|
pub struct Found {
|
||||||
@@ -165,6 +331,10 @@ impl Found {
|
|||||||
match &self.data {
|
match &self.data {
|
||||||
DataLoc::Extents { extents } => extents.iter().find(|e| !e.sparse).map(|e| e.offset),
|
DataLoc::Extents { extents } => extents.iter().find(|e| !e.sparse).map(|e| e.offset),
|
||||||
DataLoc::Resident { .. } => None,
|
DataLoc::Resident { .. } => None,
|
||||||
|
DataLoc::Parts { parts } => parts.iter().find_map(|p| match p {
|
||||||
|
Part::Run { offset, .. } => Some(*offset),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,8 +357,17 @@ pub enum FsKind {
|
|||||||
Ext,
|
Ext,
|
||||||
Apfs,
|
Apfs,
|
||||||
HfsPlus,
|
HfsPlus,
|
||||||
|
Btrfs,
|
||||||
|
Xfs,
|
||||||
|
Refs,
|
||||||
Bitlocker,
|
Bitlocker,
|
||||||
Luks,
|
Luks,
|
||||||
|
/// FileVault 2 (Core Storage).
|
||||||
|
Corestorage,
|
||||||
|
/// Membro de volume LVM (physical volume).
|
||||||
|
Lvm,
|
||||||
|
/// Membro de RAID por software (mdadm).
|
||||||
|
Raid,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,8 +382,14 @@ impl FsKind {
|
|||||||
FsKind::Ext => "ext2/3/4",
|
FsKind::Ext => "ext2/3/4",
|
||||||
FsKind::Apfs => "APFS",
|
FsKind::Apfs => "APFS",
|
||||||
FsKind::HfsPlus => "HFS+",
|
FsKind::HfsPlus => "HFS+",
|
||||||
|
FsKind::Btrfs => "Btrfs",
|
||||||
|
FsKind::Xfs => "XFS",
|
||||||
|
FsKind::Refs => "ReFS",
|
||||||
FsKind::Bitlocker => "BitLocker",
|
FsKind::Bitlocker => "BitLocker",
|
||||||
FsKind::Luks => "LUKS",
|
FsKind::Luks => "LUKS",
|
||||||
|
FsKind::Corestorage => "FileVault 2",
|
||||||
|
FsKind::Lvm => "LVM",
|
||||||
|
FsKind::Raid => "RAID (mdadm)",
|
||||||
FsKind::Unknown => "desconhecido",
|
FsKind::Unknown => "desconhecido",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,6 +399,7 @@ impl FsKind {
|
|||||||
match self {
|
match self {
|
||||||
FsKind::Bitlocker => Some("BitLocker"),
|
FsKind::Bitlocker => Some("BitLocker"),
|
||||||
FsKind::Luks => Some("LUKS"),
|
FsKind::Luks => Some("LUKS"),
|
||||||
|
FsKind::Corestorage => Some("FileVault 2"),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,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::Ntfs
|
||||||
|
| FsKind::Fat12
|
||||||
|
| FsKind::Fat16
|
||||||
|
| FsKind::Fat32
|
||||||
|
| FsKind::Exfat
|
||||||
|
| FsKind::Ext
|
||||||
|
| FsKind::HfsPlus
|
||||||
|
| FsKind::Btrfs
|
||||||
|
| FsKind::Xfs
|
||||||
|
| FsKind::Apfs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,6 +474,21 @@ pub struct ScanOptions {
|
|||||||
/// Tamanho máximo aceito para arquivos carved (bytes).
|
/// Tamanho máximo aceito para arquivos carved (bytes).
|
||||||
#[serde(default = "default_max_carve")]
|
#[serde(default = "default_max_carve")]
|
||||||
pub max_carve_size: u64,
|
pub max_carve_size: u64,
|
||||||
|
/// Membros extras (RAID/LVM) e segredos para volumes cifrados.
|
||||||
|
#[serde(default)]
|
||||||
|
pub assembly: crate::assemble::Assembly,
|
||||||
|
/// Partições que não estão na tabela do disco (volumes lógicos, desbloqueados).
|
||||||
|
#[serde(default)]
|
||||||
|
pub extra_parts: Vec<PartInfo>,
|
||||||
|
/// Partições que só contêm RAID/LVM remontado: a varredura entra pelos volumes virtuais.
|
||||||
|
#[serde(default)]
|
||||||
|
pub covered: Vec<u64>,
|
||||||
|
/// O disco principal é só membro de RAID sem metadados: varre apenas os volumes virtuais.
|
||||||
|
#[serde(default)]
|
||||||
|
pub skip_primary: bool,
|
||||||
|
/// Vídeos íntegros da mesma câmera/drone: fornecem SPS/PPS para remontar MP4 sem moov.
|
||||||
|
#[serde(default)]
|
||||||
|
pub references: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_max_carve() -> u64 {
|
fn default_max_carve() -> u64 {
|
||||||
@@ -292,6 +502,11 @@ impl Default for ScanOptions {
|
|||||||
include_allocated: false,
|
include_allocated: false,
|
||||||
categories: vec![],
|
categories: vec![],
|
||||||
max_carve_size: default_max_carve(),
|
max_carve_size: default_max_carve(),
|
||||||
|
assembly: Default::default(),
|
||||||
|
extra_parts: vec![],
|
||||||
|
covered: vec![],
|
||||||
|
skip_primary: false,
|
||||||
|
references: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
//! Disco virtual: a origem real com trechos substituídos por volumes desbloqueados,
|
||||||
|
//! membros de RAID montados ou volumes lógicos (LVM) anexados depois do fim do disco.
|
||||||
|
//!
|
||||||
|
//! Os deslocamentos continuam sendo "do disco": quem varre e quem recupera enxergam o mesmo
|
||||||
|
//! espaço de endereços, desde que a montagem seja refeita igual (a `Assembly` é determinística).
|
||||||
|
|
||||||
|
use crate::source::Source;
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Region {
|
||||||
|
pub start: u64,
|
||||||
|
pub len: u64,
|
||||||
|
pub src: Arc<dyn Source>,
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Overlay {
|
||||||
|
base: Arc<dyn Source>,
|
||||||
|
regions: Vec<Region>,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Overlay {
|
||||||
|
pub fn new(base: Arc<dyn Source>, mut regions: Vec<Region>) -> Self {
|
||||||
|
regions.sort_by_key(|r| r.start);
|
||||||
|
let size = regions
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.start + r.len)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0)
|
||||||
|
.max(base.size());
|
||||||
|
Self {
|
||||||
|
base,
|
||||||
|
regions,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn regions(&self) -> &[Region] {
|
||||||
|
&self.regions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for Overlay {
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.size || buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let want = buf.len().min((self.size - offset) as usize);
|
||||||
|
let i = self.regions.partition_point(|r| r.start + r.len <= offset);
|
||||||
|
if let Some(r) = self.regions.get(i) {
|
||||||
|
if r.start <= offset {
|
||||||
|
let n = want.min((r.start + r.len - offset) as usize);
|
||||||
|
return r.src.read_at(offset - r.start, &mut buf[..n]);
|
||||||
|
}
|
||||||
|
// Antes da próxima região: lê da base só até ela.
|
||||||
|
let n = want.min((r.start - offset) as usize);
|
||||||
|
return self.read_base(offset, &mut buf[..n]);
|
||||||
|
}
|
||||||
|
self.read_base(offset, &mut buf[..want])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Overlay {
|
||||||
|
fn read_base(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.base.size() {
|
||||||
|
// Lacuna entre a base e uma região anexada.
|
||||||
|
buf.fill(0);
|
||||||
|
return Ok(buf.len());
|
||||||
|
}
|
||||||
|
let n = buf.len().min((self.base.size() - offset) as usize);
|
||||||
|
let got = self.base.read_at(offset, &mut buf[..n])?;
|
||||||
|
if got < buf.len() && got == n {
|
||||||
|
buf[n..].fill(0);
|
||||||
|
return Ok(buf.len());
|
||||||
|
}
|
||||||
|
Ok(got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
//! AFF4: imagens de referência da Evimetry (github.com/aff4/ReferenceImages) e ida e volta.
|
||||||
|
|
||||||
|
use guara_core::aff4::{write_aff4, Aff4Options, Aff4Source};
|
||||||
|
use guara_core::source::{open_path, read_vec, MemSource, Source};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn dir() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/aff4")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reads_reference_images_and_stored_hash_matches() {
|
||||||
|
let mut seen = 0;
|
||||||
|
for name in [
|
||||||
|
"Base-Linear.aff4",
|
||||||
|
"Base-Allocated.aff4",
|
||||||
|
"Base-Linear-AllHashes.aff4",
|
||||||
|
] {
|
||||||
|
let p = dir().join(name);
|
||||||
|
if !p.exists() {
|
||||||
|
eprintln!("SKIP {name}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let a = Aff4Source::open(&p).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||||
|
assert_eq!(a.size(), 268_435_456, "{name}");
|
||||||
|
assert_eq!(
|
||||||
|
a.verify_stream_md5().unwrap(),
|
||||||
|
Some(true),
|
||||||
|
"{name}: hash do fluxo"
|
||||||
|
);
|
||||||
|
// setor 0 legível pelo caminho genérico
|
||||||
|
let s = open_path(&p).unwrap();
|
||||||
|
let head = read_vec(s.as_ref(), 0, 4096).unwrap();
|
||||||
|
assert_eq!(head.len(), 4096);
|
||||||
|
// leitura em vãos do mapa devolve zeros
|
||||||
|
let far = read_vec(s.as_ref(), 200 << 20, 512).unwrap();
|
||||||
|
assert_eq!(far.len(), 512);
|
||||||
|
eprintln!(
|
||||||
|
"{name}: {} {:?}",
|
||||||
|
a.info.get("Examinador").cloned().unwrap_or_default(),
|
||||||
|
guara_core::partition::detect_fs_in(&read_vec(s.as_ref(), 0, 4096).unwrap())
|
||||||
|
.map(|x| x.0)
|
||||||
|
);
|
||||||
|
seen += 1;
|
||||||
|
}
|
||||||
|
if seen == 0 {
|
||||||
|
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_with_snappy_blocks_and_odd_size() {
|
||||||
|
let mut x = 77u64;
|
||||||
|
let mut data: Vec<u8> = (0..5_000_001u32)
|
||||||
|
.map(|i| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
if i % 100_000 < 60_000 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(x >> 24) as u8
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
data.truncate(5_000_001);
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let p = dir.path().join("t.aff4");
|
||||||
|
let opts = Aff4Options {
|
||||||
|
case_name: "caso".into(),
|
||||||
|
examiner: "Lucas".into(),
|
||||||
|
notes: "teste".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let rep = write_aff4(
|
||||||
|
&MemSource(data.clone()),
|
||||||
|
&p,
|
||||||
|
&opts,
|
||||||
|
&|| false,
|
||||||
|
&mut |_, _| {},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let a = Aff4Source::open(&p).unwrap();
|
||||||
|
assert_eq!(a.size(), data.len() as u64);
|
||||||
|
assert_eq!(
|
||||||
|
a.info.get("md5").map(String::as_str),
|
||||||
|
Some(rep.md5.as_str())
|
||||||
|
);
|
||||||
|
assert_eq!(a.info.get("Examinador").map(String::as_str), Some("Lucas"));
|
||||||
|
assert_eq!(a.verify_stream_md5().unwrap(), Some(true));
|
||||||
|
let got = read_vec(&a, 0, data.len()).unwrap();
|
||||||
|
assert!(got == data);
|
||||||
|
let part = read_vec(&a, 123_456, 70_000).unwrap();
|
||||||
|
assert_eq!(part, &data[123_456..193_456]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
//! APFS: imagem real do dfvfs (4 checkpoints) e exclusão simulada pela troca da raiz da árvore.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use guara_core::fs::apfs::{fletcher64, live_paths};
|
||||||
|
use guara_core::source::FileSource;
|
||||||
|
use guara_core::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn img() -> Option<PathBuf> {
|
||||||
|
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/dfvfs/apfs.raw");
|
||||||
|
p.exists().then_some(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reads_the_live_tree_of_the_reference_image() {
|
||||||
|
let Some(p) = img() else { return };
|
||||||
|
let src = FileSource::open(&p).unwrap();
|
||||||
|
let files = live_paths(&src, 0);
|
||||||
|
eprintln!("{files:#?}");
|
||||||
|
assert!(!files.is_empty());
|
||||||
|
let names: Vec<&str> = files.iter().map(|f| f.0.as_str()).collect();
|
||||||
|
assert!(
|
||||||
|
names.iter().any(|n| n.ends_with("passwords.txt")),
|
||||||
|
"{names:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checksum_matches_real_blocks() {
|
||||||
|
let Some(p) = img() else { return };
|
||||||
|
let d = std::fs::read(p).unwrap();
|
||||||
|
let b = &d[0..4096];
|
||||||
|
assert_eq!(
|
||||||
|
fletcher64(&b[8..]),
|
||||||
|
u64::from_le_bytes(b[0..8].try_into().unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quick_scan_is_clean_on_pristine_volume() {
|
||||||
|
let Some(p) = img() else { return };
|
||||||
|
init();
|
||||||
|
let c = scan_path(
|
||||||
|
&p,
|
||||||
|
&ScanOptions {
|
||||||
|
mode: ScanMode::Quick,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
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<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- manipulação bruta de nós (independente do módulo) ---
|
||||||
|
|
||||||
|
fn le16(b: &[u8], o: usize) -> usize {
|
||||||
|
u16::from_le_bytes([b[o], b[o + 1]]) as usize
|
||||||
|
}
|
||||||
|
fn le32(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn le64(b: &[u8], o: usize) -> u64 {
|
||||||
|
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entry = (Vec<u8>, Vec<u8>);
|
||||||
|
|
||||||
|
fn read_node(b: &[u8]) -> (u16, Vec<Entry>) {
|
||||||
|
let flags = le16(b, 32) as u16;
|
||||||
|
let n = le32(b, 36) as usize;
|
||||||
|
let (toc_off, toc_len) = (le16(b, 40), le16(b, 42));
|
||||||
|
let root = flags & 1 != 0;
|
||||||
|
let key_base = 56 + toc_off + toc_len;
|
||||||
|
let val_end = b.len() - if root { 40 } else { 0 };
|
||||||
|
let mut out = vec![];
|
||||||
|
for i in 0..n {
|
||||||
|
let o = 56 + toc_off + i * 8;
|
||||||
|
let (ko, kl, vo, vl) = (le16(b, o), le16(b, o + 2), le16(b, o + 4), le16(b, o + 6));
|
||||||
|
out.push((
|
||||||
|
b[key_base + ko..key_base + ko + kl].to_vec(),
|
||||||
|
b[val_end - vo..val_end - vo + vl].to_vec(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(flags, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_node(old: &[u8], entries: &[Entry]) -> Vec<u8> {
|
||||||
|
let mut b = vec![0u8; old.len()];
|
||||||
|
b[..56].copy_from_slice(&old[..56]);
|
||||||
|
let flags = le16(old, 32) as u16;
|
||||||
|
let root = flags & 1 != 0;
|
||||||
|
if root {
|
||||||
|
let l = old.len();
|
||||||
|
b[l - 40..].copy_from_slice(&old[l - 40..]);
|
||||||
|
}
|
||||||
|
let n = entries.len();
|
||||||
|
b[36..40].copy_from_slice(&(n as u32).to_le_bytes());
|
||||||
|
b[40..42].copy_from_slice(&0u16.to_le_bytes());
|
||||||
|
b[42..44].copy_from_slice(&((n * 8) as u16).to_le_bytes());
|
||||||
|
let key_base = 56 + n * 8;
|
||||||
|
let val_end = old.len() - if root { 40 } else { 0 };
|
||||||
|
let (mut kpos, mut vpos) = (0usize, 0usize);
|
||||||
|
for (i, (k, v)) in entries.iter().enumerate() {
|
||||||
|
vpos += v.len();
|
||||||
|
let o = 56 + i * 8;
|
||||||
|
b[o..o + 2].copy_from_slice(&(kpos as u16).to_le_bytes());
|
||||||
|
b[o + 2..o + 4].copy_from_slice(&(k.len() as u16).to_le_bytes());
|
||||||
|
b[o + 4..o + 6].copy_from_slice(&(vpos as u16).to_le_bytes());
|
||||||
|
b[o + 6..o + 8].copy_from_slice(&(v.len() as u16).to_le_bytes());
|
||||||
|
b[key_base + kpos..key_base + kpos + k.len()].copy_from_slice(k);
|
||||||
|
b[val_end - vpos..val_end - vpos + v.len()].copy_from_slice(v);
|
||||||
|
kpos += k.len();
|
||||||
|
}
|
||||||
|
seal(&mut b);
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seal(b: &mut [u8]) {
|
||||||
|
let c = fletcher64(&b[8..]);
|
||||||
|
b[0..8].copy_from_slice(&c.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_removed_from_the_current_tree_survives_in_the_old_node() {
|
||||||
|
let Some(p) = img() else { return };
|
||||||
|
let mut d = std::fs::read(&p).unwrap();
|
||||||
|
let bs = 4096usize;
|
||||||
|
let blk = |d: &[u8], n: u64| d[n as usize * bs..(n as usize + 1) * bs].to_vec();
|
||||||
|
// Superbloco mais recente do contêiner.
|
||||||
|
let (desc_blocks, desc_base) = (le32(&d, 104) as u64, le64(&d, 112));
|
||||||
|
let mut nx = d[..bs].to_vec();
|
||||||
|
for i in 0..desc_blocks {
|
||||||
|
let b = blk(&d, desc_base + i);
|
||||||
|
if &b[32..36] == b"NXSB" && le64(&b, 16) > le64(&nx, 16) {
|
||||||
|
nx = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// omap do contêiner -> volume
|
||||||
|
let cont_omap = blk(&d, le64(&nx, 160));
|
||||||
|
let cont_tree = blk(&d, le64(&cont_omap, 48));
|
||||||
|
let (_, cont_entries) = read_node(&{
|
||||||
|
// omap usa chaves fixas (16/16): reaproveita o leitor variável convertendo a tabela
|
||||||
|
let mut c = cont_tree.clone();
|
||||||
|
fixed_to_var(&mut c);
|
||||||
|
c
|
||||||
|
});
|
||||||
|
let vol_oid = le64(&nx, 184);
|
||||||
|
let vol_paddr = cont_entries
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, _)| le64(k, 0) == vol_oid)
|
||||||
|
.map(|(_, v)| le64(v, 8))
|
||||||
|
.next_back()
|
||||||
|
.unwrap();
|
||||||
|
let vsb = blk(&d, vol_paddr);
|
||||||
|
let (vol_omap_paddr, root_oid) = (le64(&vsb, 128), le64(&vsb, 136));
|
||||||
|
let vol_omap = blk(&d, vol_omap_paddr);
|
||||||
|
let vol_tree_paddr = le64(&vol_omap, 48);
|
||||||
|
let mut vt = blk(&d, vol_tree_paddr);
|
||||||
|
let vt_orig = vt.clone();
|
||||||
|
fixed_to_var(&mut vt);
|
||||||
|
let (_, omap_entries) = read_node(&vt);
|
||||||
|
let (root_idx, root_paddr) = omap_entries
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, (k, _))| le64(k, 0) == root_oid)
|
||||||
|
.map(|(i, (_, v))| (i, le64(v, 8)))
|
||||||
|
.next_back()
|
||||||
|
.unwrap();
|
||||||
|
let root = blk(&d, root_paddr);
|
||||||
|
assert_eq!(le16(&root, 34), 0, "raiz deveria ser folha nesta imagem");
|
||||||
|
let (_, recs) = read_node(&root);
|
||||||
|
// inode do arquivo
|
||||||
|
let victim = b"passwords.txt";
|
||||||
|
let ino = recs
|
||||||
|
.iter()
|
||||||
|
.find_map(|(k, v)| {
|
||||||
|
let hdr = le64(k, 0);
|
||||||
|
((hdr >> 60) == 9 && k.len() > 12 && k[12..].starts_with(victim)).then(|| le64(v, 0))
|
||||||
|
})
|
||||||
|
.expect("entrada de diretório");
|
||||||
|
let private_id = recs
|
||||||
|
.iter()
|
||||||
|
.find_map(|(k, v)| {
|
||||||
|
((le64(k, 0) >> 60) == 3 && (le64(k, 0) & 0x0FFF_FFFF_FFFF_FFFF) == ino)
|
||||||
|
.then(|| le64(v, 8))
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let kept: Vec<Entry> = recs
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, v)| {
|
||||||
|
let hdr = le64(k, 0);
|
||||||
|
let (id, ty) = (hdr & 0x0FFF_FFFF_FFFF_FFFF, hdr >> 60);
|
||||||
|
let drec_of_victim =
|
||||||
|
ty == 9 && k.len() > 12 && k[12..].starts_with(victim) && le64(v, 0) == ino;
|
||||||
|
!(id == ino && ty != 9 || id == private_id && ty == 8 || drec_of_victim)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
assert!(kept.len() < recs.len());
|
||||||
|
let newnode = write_node(&root, &kept);
|
||||||
|
// bloco livre para a nova versão
|
||||||
|
let free = (10..d.len() / bs)
|
||||||
|
.find(|&n| d[n * bs..(n + 1) * bs].iter().all(|&x| x == 0) && n != vol_tree_paddr as usize)
|
||||||
|
.expect("bloco livre");
|
||||||
|
d[free * bs..(free + 1) * bs].copy_from_slice(&newnode);
|
||||||
|
// aponta o mapa de objetos para a nova versão (mesmo xid), editando o valor da entrada
|
||||||
|
let toc_off = le16(&vt_orig, 40);
|
||||||
|
let key_base = 56 + toc_off + le16(&vt_orig, 42);
|
||||||
|
let _ = key_base;
|
||||||
|
let val_end = bs - 40;
|
||||||
|
let vo = le16(&vt_orig, 56 + toc_off + root_idx * 4 + 2);
|
||||||
|
let at = vol_tree_paddr as usize * bs + val_end - vo + 8;
|
||||||
|
d[at..at + 8].copy_from_slice(&(free as u64).to_le_bytes());
|
||||||
|
let mut ob = d[vol_tree_paddr as usize * bs..(vol_tree_paddr as usize + 1) * bs].to_vec();
|
||||||
|
seal(&mut ob);
|
||||||
|
d[vol_tree_paddr as usize * bs..(vol_tree_paddr as usize + 1) * bs].copy_from_slice(&ob);
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let p2 = dir.path().join("a.img");
|
||||||
|
std::fs::write(&p2, &d).unwrap();
|
||||||
|
// O arquivo sumiu da árvore atual...
|
||||||
|
let src = FileSource::open(&p2).unwrap();
|
||||||
|
assert!(!live_paths(&src, 0).iter().any(|f| f.0 == "passwords.txt"));
|
||||||
|
init();
|
||||||
|
let c = scan_path(
|
||||||
|
&p2,
|
||||||
|
&ScanOptions {
|
||||||
|
mode: ScanMode::Deep,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let f = c
|
||||||
|
.found
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.origin == Origin::Apfs && f.name == "passwords.txt")
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"{:?}",
|
||||||
|
c.found
|
||||||
|
.iter()
|
||||||
|
.map(|f| (&f.name, f.origin))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
assert_eq!(f.size, 116);
|
||||||
|
let out = dir.path().join("out");
|
||||||
|
let rep = recover_all(&p2, std::slice::from_ref(f), &out);
|
||||||
|
assert!(rep.failed.is_empty());
|
||||||
|
let rp = PathBuf::from(&rep.recovered[0].path);
|
||||||
|
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
|
||||||
|
assert_eq!(got.len(), 116);
|
||||||
|
assert!(
|
||||||
|
String::from_utf8_lossy(&got)
|
||||||
|
.chars()
|
||||||
|
.all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t'),
|
||||||
|
"{got:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converte a tabela de entradas fixas (4 bytes) de um nó de omap para o formato variável (8 bytes) em memória.
|
||||||
|
fn fixed_to_var(b: &mut Vec<u8>) {
|
||||||
|
let flags = le16(b, 32) as u16;
|
||||||
|
if flags & 4 == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let n = le32(b, 36) as usize;
|
||||||
|
let (toc_off, toc_len) = (le16(b, 40), le16(b, 42));
|
||||||
|
let key_base = 56 + toc_off + toc_len;
|
||||||
|
let root = flags & 1 != 0;
|
||||||
|
let val_end = b.len() - if root { 40 } else { 0 };
|
||||||
|
let mut entries: Vec<(Vec<u8>, Vec<u8>)> = vec![];
|
||||||
|
for i in 0..n {
|
||||||
|
let o = 56 + toc_off + i * 4;
|
||||||
|
let (ko, vo) = (le16(b, o), le16(b, o + 2));
|
||||||
|
entries.push((
|
||||||
|
b[key_base + ko..key_base + ko + 16].to_vec(),
|
||||||
|
b[val_end - vo..val_end - vo + 16].to_vec(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut nb = b.clone();
|
||||||
|
nb[32..34].copy_from_slice(&(flags & !4).to_le_bytes());
|
||||||
|
*b = write_node_flagsless(&nb, &entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_node_flagsless(old: &[u8], entries: &[Entry]) -> Vec<u8> {
|
||||||
|
let mut b = write_node(old, entries);
|
||||||
|
// write_node usa as flags de `old`
|
||||||
|
let fl = le16(&b, 32) as u16;
|
||||||
|
b[32..34].copy_from_slice(&fl.to_le_bytes());
|
||||||
|
b
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
//! BitLocker: imagens reais do conjunto de testes do cryptsetup (GitLab), com senhas conhecidas.
|
||||||
|
//! Baixe com `scripts/fetch-testdata.sh`; sem elas os testes são pulados.
|
||||||
|
|
||||||
|
use guara_core::crypt::{bitlocker, Secret, UnlockError};
|
||||||
|
use guara_core::partition::detect_fs_in;
|
||||||
|
use guara_core::source::{read_vec, FileSource, Source};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn dir() -> Option<PathBuf> {
|
||||||
|
let d = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/bitlk-images");
|
||||||
|
d.join("images.conf").exists().then_some(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn conf(d: &std::path::Path) -> BTreeMap<String, BTreeMap<String, String>> {
|
||||||
|
let mut m: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
|
||||||
|
let mut cur = String::new();
|
||||||
|
for l in std::fs::read_to_string(d.join("images.conf"))
|
||||||
|
.unwrap()
|
||||||
|
.lines()
|
||||||
|
{
|
||||||
|
let l = l.trim();
|
||||||
|
if let Some(n) = l.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
|
||||||
|
cur = n.to_string();
|
||||||
|
} else if let Some((k, v)) = l.split_once('=') {
|
||||||
|
m.entry(cur.clone()).or_default().insert(k.into(), v.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_fs(src: &dyn Source, what: &str) {
|
||||||
|
// Imagens "encrypt-on-write" não têm sistema de arquivos reconhecível (UUID vazio no images.conf).
|
||||||
|
if what.contains("-eow ") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let head = read_vec(src, 0, 4096).unwrap();
|
||||||
|
let fs = detect_fs_in(&head);
|
||||||
|
assert!(
|
||||||
|
fs.is_some(),
|
||||||
|
"{what}: setor 0 aberto não é um sistema de arquivos: {:02x?}",
|
||||||
|
&head[..32]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opens_all_reference_images_with_password_and_recovery_key() {
|
||||||
|
let Some(d) = dir() else {
|
||||||
|
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut opened = 0;
|
||||||
|
for (name, c) in conf(&d) {
|
||||||
|
let path = d.join(format!("{name}.img"));
|
||||||
|
if !path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let src: Arc<dyn Source> = Arc::new(FileSource::open(&path).unwrap());
|
||||||
|
let pw = c.get("PW").cloned().unwrap_or_default();
|
||||||
|
let rp = c.get("RP").cloned().unwrap_or_default();
|
||||||
|
// Proteção suspensa (chave limpa): abre sem segredo nenhum.
|
||||||
|
if let Ok(u) = bitlocker::unlock(src.clone(), &[]) {
|
||||||
|
check_fs(u.source.as_ref(), &format!("{name} (chave limpa)"));
|
||||||
|
eprintln!("ok {name}: chave limpa");
|
||||||
|
opened += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match bitlocker::unlock(src.clone(), &[Secret::Password("errada".into())]) {
|
||||||
|
Err(UnlockError::WrongSecret) | Err(UnlockError::Unsupported(_)) => {}
|
||||||
|
Err(e) => panic!("{name}: {e}"),
|
||||||
|
Ok(_) => panic!("{name}: abriu com senha errada"),
|
||||||
|
}
|
||||||
|
let mut views = vec![];
|
||||||
|
if !pw.is_empty() {
|
||||||
|
let u = bitlocker::unlock(src.clone(), &[Secret::Password(pw.clone())])
|
||||||
|
.unwrap_or_else(|e| panic!("{name} senha: {e}"));
|
||||||
|
check_fs(u.source.as_ref(), &format!("{name} (senha)"));
|
||||||
|
views.push(u);
|
||||||
|
}
|
||||||
|
if !rp.is_empty() {
|
||||||
|
let u = bitlocker::unlock(src.clone(), &[Secret::RecoveryKey(rp.clone())])
|
||||||
|
.unwrap_or_else(|e| panic!("{name} recuperação: {e}"));
|
||||||
|
check_fs(u.source.as_ref(), &format!("{name} (recuperação)"));
|
||||||
|
views.push(u);
|
||||||
|
}
|
||||||
|
if let Some(g) = c.get("SK_VMK_GUID") {
|
||||||
|
let f = d.join(format!("{}.BEK", g.to_uppercase()));
|
||||||
|
if f.exists() {
|
||||||
|
let u =
|
||||||
|
bitlocker::unlock(src.clone(), &[Secret::KeyFile(std::fs::read(f).unwrap())])
|
||||||
|
.unwrap_or_else(|e| panic!("{name} BEK: {e}"));
|
||||||
|
check_fs(u.source.as_ref(), &format!("{name} (BEK)"));
|
||||||
|
views.push(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(!views.is_empty(), "{name}: nenhum protetor testável");
|
||||||
|
let first = read_vec(views[0].source.as_ref(), 0, 1 << 20).unwrap();
|
||||||
|
for v in &views[1..] {
|
||||||
|
assert!(
|
||||||
|
read_vec(v.source.as_ref(), 0, 1 << 20).unwrap() == first,
|
||||||
|
"{name}: protetores divergem"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
opened += 1;
|
||||||
|
eprintln!(
|
||||||
|
"ok {name}: {} ({} protetores)",
|
||||||
|
views[0].description,
|
||||||
|
views.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(opened > 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
//! Btrfs: imagens criadas com mkfs.btrfs --rootdir; exclusão simulada como o COW faz
|
||||||
|
//! (nova versão da folha em outro bloco, a antiga continua no disco).
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use guara_core::fs::btrfs::{crc32c, live_paths, lzo1x_decompress};
|
||||||
|
use guara_core::source::FileSource;
|
||||||
|
use guara_core::*;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn have(t: &str) -> bool {
|
||||||
|
Command::new("sh")
|
||||||
|
.args(["-c", &format!("command -v {t}")])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(seed: u64, n: usize) -> Vec<u8> {
|
||||||
|
let mut x = seed | 1;
|
||||||
|
(0..n)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Img {
|
||||||
|
_dir: tempfile::TempDir,
|
||||||
|
path: PathBuf,
|
||||||
|
files: Vec<(&'static str, Vec<u8>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build() -> Option<Img> {
|
||||||
|
if !have("mkfs.btrfs") {
|
||||||
|
eprintln!("SKIP: mkfs.btrfs ausente");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
std::fs::create_dir_all(root.join("docs/sub")).unwrap();
|
||||||
|
std::fs::create_dir_all(root.join("fotos")).unwrap();
|
||||||
|
let files = vec![
|
||||||
|
("docs/relatorio.dat", data(1, 150_000)),
|
||||||
|
("docs/sub/contrato.dat", data(2, 9_000)),
|
||||||
|
("fotos/viagem.dat", data(3, 700_000)),
|
||||||
|
("fotos/fica.dat", data(4, 30_000)),
|
||||||
|
("pequeno.txt", b"conteudo pequeno inline".to_vec()),
|
||||||
|
];
|
||||||
|
for (p, c) in &files {
|
||||||
|
std::fs::write(root.join(p), c).unwrap();
|
||||||
|
}
|
||||||
|
let path = dir.path().join("b.img");
|
||||||
|
std::fs::File::create(&path)
|
||||||
|
.unwrap()
|
||||||
|
.set_len(128 << 20)
|
||||||
|
.unwrap();
|
||||||
|
let o = Command::new("mkfs.btrfs")
|
||||||
|
.args(["-q", "-f", "--rootdir"])
|
||||||
|
.arg(&root)
|
||||||
|
.arg(&path)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
|
||||||
|
Some(Img {
|
||||||
|
_dir: dir,
|
||||||
|
path,
|
||||||
|
files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lists_the_live_tree_with_paths_and_sizes() {
|
||||||
|
let Some(i) = build() else { return };
|
||||||
|
let src = FileSource::open(&i.path).unwrap();
|
||||||
|
let listed = live_paths(&src, 0);
|
||||||
|
for (p, c) in &i.files {
|
||||||
|
let f = listed
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.0 == *p)
|
||||||
|
.unwrap_or_else(|| panic!("{p} ausente: {listed:?}"));
|
||||||
|
assert_eq!(f.1, c.len() as u64, "{p}");
|
||||||
|
assert!(!f.2);
|
||||||
|
}
|
||||||
|
assert!(listed.iter().any(|x| x.0 == "docs/sub" && x.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- manipulação bruta de folhas (independente do módulo) ---
|
||||||
|
|
||||||
|
struct Leaf {
|
||||||
|
raw: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Leaf {
|
||||||
|
fn nritems(&self) -> usize {
|
||||||
|
u32::from_le_bytes(self.raw[96..100].try_into().unwrap()) as usize
|
||||||
|
}
|
||||||
|
fn item(&self, i: usize) -> (u64, u8, u64, usize, usize) {
|
||||||
|
let o = 101 + i * 25;
|
||||||
|
let r = &self.raw;
|
||||||
|
(
|
||||||
|
u64::from_le_bytes(r[o..o + 8].try_into().unwrap()),
|
||||||
|
r[o + 8],
|
||||||
|
u64::from_le_bytes(r[o + 9..o + 17].try_into().unwrap()),
|
||||||
|
u32::from_le_bytes(r[o + 17..o + 21].try_into().unwrap()) as usize,
|
||||||
|
u32::from_le_bytes(r[o + 21..o + 25].try_into().unwrap()) as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn data(&self, i: usize) -> &[u8] {
|
||||||
|
let (_, _, _, off, size) = self.item(i);
|
||||||
|
&self.raw[101 + off..101 + off + size]
|
||||||
|
}
|
||||||
|
fn seal(&mut self) {
|
||||||
|
let c = crc32c(&self.raw[32..]);
|
||||||
|
self.raw[0..4].copy_from_slice(&c.to_le_bytes());
|
||||||
|
}
|
||||||
|
/// Nova versão da folha sem os itens que `drop` rejeita, num novo endereço lógico.
|
||||||
|
fn without(&self, drop: impl Fn(u64, u8) -> bool, new_bytenr: u64) -> Leaf {
|
||||||
|
let mut kept: Vec<(usize, Vec<u8>)> = vec![];
|
||||||
|
for i in 0..self.nritems() {
|
||||||
|
let (obj, ty, _, _, _) = self.item(i);
|
||||||
|
if !drop(obj, ty) {
|
||||||
|
kept.push((i, self.data(i).to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut raw = vec![0u8; self.raw.len()];
|
||||||
|
raw[..101].copy_from_slice(&self.raw[..101]);
|
||||||
|
raw[48..56].copy_from_slice(&new_bytenr.to_le_bytes());
|
||||||
|
let gen = u64::from_le_bytes(raw[80..88].try_into().unwrap()) + 1;
|
||||||
|
raw[80..88].copy_from_slice(&gen.to_le_bytes());
|
||||||
|
raw[96..100].copy_from_slice(&(kept.len() as u32).to_le_bytes());
|
||||||
|
let mut end = raw.len() - 101;
|
||||||
|
for (n, (i, d)) in kept.iter().enumerate() {
|
||||||
|
let o = 101 + n * 25;
|
||||||
|
raw[o..o + 17].copy_from_slice(&self.raw[101 + i * 25..101 + i * 25 + 17]);
|
||||||
|
end -= d.len();
|
||||||
|
raw[o + 17..o + 21].copy_from_slice(&(end as u32).to_le_bytes());
|
||||||
|
raw[o + 21..o + 25].copy_from_slice(&(d.len() as u32).to_le_bytes());
|
||||||
|
raw[101 + end..101 + end + d.len()].copy_from_slice(d);
|
||||||
|
}
|
||||||
|
let mut l = Leaf { raw };
|
||||||
|
l.seal();
|
||||||
|
l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn superblock(img: &[u8]) -> &[u8] {
|
||||||
|
&img[0x10000..0x11000]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mapa lógico -> físico dos chunks de sistema (suficiente para imagens pequenas de um dispositivo).
|
||||||
|
fn chunk_map(img: &[u8]) -> Vec<(u64, u64, u64)> {
|
||||||
|
let sb = superblock(img);
|
||||||
|
let n = u32::from_le_bytes(sb[160..164].try_into().unwrap()) as usize;
|
||||||
|
let arr = &sb[0x32B..0x32B + n];
|
||||||
|
let mut p = 0;
|
||||||
|
let mut out = vec![];
|
||||||
|
while p + 17 + 80 <= arr.len() {
|
||||||
|
let logical = u64::from_le_bytes(arr[p + 9..p + 17].try_into().unwrap());
|
||||||
|
let len = u64::from_le_bytes(arr[p + 17..p + 25].try_into().unwrap());
|
||||||
|
let phys = u64::from_le_bytes(arr[p + 17 + 48 + 8..p + 17 + 48 + 16].try_into().unwrap());
|
||||||
|
out.push((logical, len, phys));
|
||||||
|
let ns = u16::from_le_bytes(arr[p + 17 + 44..p + 17 + 46].try_into().unwrap()) as usize;
|
||||||
|
p += 17 + 48 + ns * 32;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
|
||||||
|
let Some(i) = build() else { return };
|
||||||
|
let mut img = std::fs::read(&i.path).unwrap();
|
||||||
|
let sb = superblock(&img).to_vec();
|
||||||
|
let nodesize = u32::from_le_bytes(sb[148..152].try_into().unwrap()) as usize;
|
||||||
|
let src = FileSource::open(&i.path).unwrap();
|
||||||
|
let t = guara_core::fs::btrfs::locate_trees(&src, 0).expect("árvores");
|
||||||
|
assert_eq!(t.nodesize as usize, nodesize);
|
||||||
|
let fs_leaf = (t.fs_tree as usize, t.fs_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 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");
|
||||||
|
c.0 + (phys as u64 - c.2)
|
||||||
|
};
|
||||||
|
let fl = Leaf {
|
||||||
|
raw: img[fs_leaf.0..fs_leaf.0 + nodesize].to_vec(),
|
||||||
|
};
|
||||||
|
// inode do arquivo apagado
|
||||||
|
let victim = "relatorio.dat";
|
||||||
|
let mut ino = 0;
|
||||||
|
for k in 0..fl.nritems() {
|
||||||
|
let (obj, ty, _, _, _) = fl.item(k);
|
||||||
|
if ty == 12 {
|
||||||
|
let d = fl.data(k);
|
||||||
|
let nl = u16::from_le_bytes([d[8], d[9]]) as usize;
|
||||||
|
if &d[10..10 + nl] == victim.as_bytes() {
|
||||||
|
ino = obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(ino > 256);
|
||||||
|
// extent de dados do arquivo (para liberar no extent tree)
|
||||||
|
let mut data_bytenr = 0;
|
||||||
|
for k in 0..fl.nritems() {
|
||||||
|
let (obj, ty, _, _, _) = fl.item(k);
|
||||||
|
if obj == ino && ty == 108 {
|
||||||
|
data_bytenr = u64::from_le_bytes(fl.data(k)[21..29].try_into().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(data_bytenr > 0);
|
||||||
|
// Novo bloco livre para a nova versão: primeiro bloco do metadata sem folha válida e todo zero.
|
||||||
|
let free_slot = (0..img.len() / nodesize)
|
||||||
|
.map(|n| n * nodesize)
|
||||||
|
.find(|&o| {
|
||||||
|
o > fs_leaf.0
|
||||||
|
&& img[o..o + nodesize].iter().all(|&x| x == 0)
|
||||||
|
&& t.chunks
|
||||||
|
.iter()
|
||||||
|
.any(|c| (o as u64) >= c.2 && (o as u64) < c.2 + c.1)
|
||||||
|
&& o < fs_leaf.0 + (8 << 20)
|
||||||
|
})
|
||||||
|
.expect("bloco livre");
|
||||||
|
// logical = bytenr da folha antiga + (offset físico - offset físico antigo): mesmo chunk
|
||||||
|
let new_bytenr = to_logical(free_slot);
|
||||||
|
let newleaf = fl.without(|o, _| o == ino, new_bytenr);
|
||||||
|
img[free_slot..free_slot + nodesize].copy_from_slice(&newleaf.raw);
|
||||||
|
// Aponta o ROOT_ITEM da árvore 5 para a nova versão (e sobe a geração), na folha da árvore raiz.
|
||||||
|
let mut rl = Leaf {
|
||||||
|
raw: img[root_leaf.0..root_leaf.0 + nodesize].to_vec(),
|
||||||
|
};
|
||||||
|
for k in 0..rl.nritems() {
|
||||||
|
let (obj, ty, _, off, _) = rl.item(k);
|
||||||
|
if obj == 5 && ty == 132 {
|
||||||
|
let at = 101 + off + 176;
|
||||||
|
rl.raw[at..at + 8].copy_from_slice(&new_bytenr.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.seal();
|
||||||
|
img[root_leaf.0..root_leaf.0 + nodesize].copy_from_slice(&rl.raw);
|
||||||
|
// Libera o extent de dados no extent tree (em outro endereço da mesma folha).
|
||||||
|
let el = Leaf {
|
||||||
|
raw: img[ext_leaf.0..ext_leaf.0 + nodesize].to_vec(),
|
||||||
|
};
|
||||||
|
let newext = el.without(|o, ty| o == data_bytenr && ty == 168, ext_leaf.1);
|
||||||
|
let _ = to_logical;
|
||||||
|
img[ext_leaf.0..ext_leaf.0 + nodesize].copy_from_slice(&newext.raw);
|
||||||
|
let _ = chunk_map(&img);
|
||||||
|
|
||||||
|
let p2 = i._dir.path().join("deleted.img");
|
||||||
|
std::fs::write(&p2, &img).unwrap();
|
||||||
|
init();
|
||||||
|
let c = scan_path(
|
||||||
|
&p2,
|
||||||
|
&ScanOptions {
|
||||||
|
mode: ScanMode::Deep,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let f = c
|
||||||
|
.found
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == victim && f.origin == Origin::Btrfs)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"não achou: {:?}",
|
||||||
|
c.found
|
||||||
|
.iter()
|
||||||
|
.map(|f| (&f.name, f.origin))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
assert_eq!(f.size, 150_000);
|
||||||
|
assert_eq!(f.dir.as_deref(), Some("/docs"));
|
||||||
|
assert!(
|
||||||
|
f.confidence >= Confidence::Medium && f.note.as_deref().unwrap_or("").contains("livres"),
|
||||||
|
"{:?}",
|
||||||
|
f.note
|
||||||
|
);
|
||||||
|
let out = i._dir.path().join("out");
|
||||||
|
let rep = recover_all(&p2, std::slice::from_ref(f), &out);
|
||||||
|
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
||||||
|
let rp = PathBuf::from(&rep.recovered[0].path);
|
||||||
|
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
|
||||||
|
assert!(got == i.files[0].1);
|
||||||
|
// Os arquivos que continuam existindo não aparecem como apagados.
|
||||||
|
assert!(!c
|
||||||
|
.found
|
||||||
|
.iter()
|
||||||
|
.any(|f| f.name == "fica.dat" && f.origin == Origin::Btrfs));
|
||||||
|
let _ = Path::new("");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lzo_literals_and_a_match() {
|
||||||
|
// 20 literais + fim
|
||||||
|
let mut s = vec![17 + 20];
|
||||||
|
s.extend((0..20u8).map(|i| b'a' + i));
|
||||||
|
s.extend_from_slice(&[0x11, 0, 0]);
|
||||||
|
assert_eq!(
|
||||||
|
lzo1x_decompress(&s, 4096).unwrap(),
|
||||||
|
(0..20u8).map(|i| b'a' + i).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
// "abcd" + M2 (dist 4, len 4) + fim
|
||||||
|
let s = vec![17 + 4, b'a', b'b', b'c', b'd', 0x6C, 0x00, 0x11, 0, 0];
|
||||||
|
assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), b"abcdabcd");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn packed_extents_decode_zlib_and_zstd() {
|
||||||
|
use guara_core::source::MemSource;
|
||||||
|
use guara_core::{Packed, PackedCodec};
|
||||||
|
use std::io::Write;
|
||||||
|
let plain: Vec<u8> = (0..100_000u32).map(|i| (i % 251) as u8).collect();
|
||||||
|
// zlib
|
||||||
|
let mut e = flate2::write::ZlibEncoder::new(vec![], flate2::Compression::default());
|
||||||
|
e.write_all(&plain).unwrap();
|
||||||
|
let z = e.finish().unwrap();
|
||||||
|
let mut disk = vec![0u8; 4096];
|
||||||
|
disk.extend_from_slice(&z);
|
||||||
|
let p = Packed {
|
||||||
|
offset: 4096,
|
||||||
|
disk_len: z.len() as u64,
|
||||||
|
ram_len: plain.len() as u64,
|
||||||
|
skip: 1000,
|
||||||
|
len: 5000,
|
||||||
|
codec: PackedCodec::Zlib,
|
||||||
|
};
|
||||||
|
assert_eq!(p.read(&MemSource(disk)).unwrap(), &plain[1000..6000]);
|
||||||
|
// zstd, se o utilitário existir
|
||||||
|
if have("zstd") {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let f = dir.path().join("p.bin");
|
||||||
|
std::fs::write(&f, &plain).unwrap();
|
||||||
|
assert!(Command::new("zstd")
|
||||||
|
.args(["-q", "-f"])
|
||||||
|
.arg(&f)
|
||||||
|
.status()
|
||||||
|
.unwrap()
|
||||||
|
.success());
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
assert!(p.read(&MemSource(z)).unwrap() == plain);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,12 @@ impl Sink for Collect {
|
|||||||
f.confidence = f.confidence.max(c);
|
f.confidence = f.confidence.max(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn replace(&mut self, f: Found) {
|
||||||
|
match self.found.iter_mut().find(|x| x.id == f.id) {
|
||||||
|
Some(x) => *x = f,
|
||||||
|
None => self.found.push(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
fn progress(&mut self, _: Progress) {}
|
fn progress(&mut self, _: Progress) {}
|
||||||
fn warning(&mut self, w: String) {
|
fn warning(&mut self, w: String) {
|
||||||
self.warnings.push(w);
|
self.warnings.push(w);
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
use guara_core::defrag::*;
|
||||||
|
use guara_core::source::MemSource;
|
||||||
|
use guara_core::types::Extent;
|
||||||
|
|
||||||
|
fn photo(w: u16, h: u16, seed: u64, shift: i32) -> Vec<u8> {
|
||||||
|
photo_n(w, h, seed, shift, 31)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn photo_n(w: u16, h: u16, seed: u64, shift: i32, nmask: u64) -> Vec<u8> {
|
||||||
|
let mut rgb = Vec::with_capacity(w as usize * h as usize * 3);
|
||||||
|
let mut x = seed | 1;
|
||||||
|
for yy in 0..h as i32 {
|
||||||
|
for xx in 0..w as i32 {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
let noise = ((x >> 40) & nmask) as i32;
|
||||||
|
let fx = xx as f32 / w as f32;
|
||||||
|
let fy = yy as f32 / h as f32;
|
||||||
|
let wave = ((fx * 9.0 + seed as f32 * 0.37).sin() * (fy * 7.0 + 1.3).cos() * 40.0)
|
||||||
|
as i32
|
||||||
|
+ ((fx * 31.0).sin() * 8.0) as i32;
|
||||||
|
let 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 v = (base + blob + noise).clamp(0, 255) as u8;
|
||||||
|
rgb.extend_from_slice(&[v, v.saturating_sub(10), (v as i32 / 2 + noise) as u8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut out = vec![];
|
||||||
|
jpeg_encoder::Encoder::new(&mut out, 92)
|
||||||
|
.encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb)
|
||||||
|
.unwrap();
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn noise(n: usize, seed: u64) -> Vec<u8> {
|
||||||
|
let mut x = seed | 1;
|
||||||
|
(0..n)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad(v: &mut Vec<u8>) {
|
||||||
|
v.resize(v.len().div_ceil(4096) * 4096, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rebuilds_a_jpeg_split_around_another_one() {
|
||||||
|
let a = photo(1024, 768, 7, 0);
|
||||||
|
let b = photo(800, 600, 99, 40);
|
||||||
|
let split = 14 * 4096;
|
||||||
|
assert!(a.len() > split + 3 * 4096, "A pequena: {}", a.len());
|
||||||
|
let mut disk: Vec<u8> = vec![0u8; 64 * 4096];
|
||||||
|
let a_off = disk.len() as u64;
|
||||||
|
disk.extend_from_slice(&a[..split]);
|
||||||
|
let b_off = disk.len() as u64;
|
||||||
|
let mut bb = b.clone();
|
||||||
|
pad(&mut bb);
|
||||||
|
disk.extend_from_slice(&bb);
|
||||||
|
disk.extend(noise(20 * 4096, 5)); // lixo
|
||||||
|
let a2_off = disk.len() as u64;
|
||||||
|
let mut rest = a[split..].to_vec();
|
||||||
|
pad(&mut rest);
|
||||||
|
disk.extend_from_slice(&rest);
|
||||||
|
disk.extend(noise(10 * 4096, 6));
|
||||||
|
let src = MemSource(disk.clone());
|
||||||
|
|
||||||
|
let mut idx = ClusterIndex::new(disk.len() as u64);
|
||||||
|
idx.scan_chunk(0, &disk);
|
||||||
|
// O fragmento correto precisa estar marcado.
|
||||||
|
assert!(idx.get(a2_off));
|
||||||
|
eprintln!("TRUE a2_off={a2_off}");
|
||||||
|
let pool = Pool {
|
||||||
|
index: &idx,
|
||||||
|
used: &[Extent::new(b_off, bb.len() as u64)],
|
||||||
|
};
|
||||||
|
match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) {
|
||||||
|
Analysis::Rebuilt { extents, size } => {
|
||||||
|
assert_eq!(size as usize, a.len(), "tamanho");
|
||||||
|
assert_eq!(extents.len(), 2);
|
||||||
|
assert_eq!(extents[0], Extent::new(a_off, split as u64));
|
||||||
|
assert_eq!(extents[1].offset, a2_off);
|
||||||
|
let mut got = disk[a_off as usize..a_off as usize + split].to_vec();
|
||||||
|
got.extend_from_slice(
|
||||||
|
&disk[a2_off as usize..a2_off as usize + (size as usize - split)],
|
||||||
|
);
|
||||||
|
assert!(got == a, "conteúdo recomposto difere");
|
||||||
|
}
|
||||||
|
other => panic!("esperava Rebuilt, veio {other:?}"),
|
||||||
|
}
|
||||||
|
// O outro JPEG, íntegro, continua íntegro.
|
||||||
|
let pool2 = Pool {
|
||||||
|
index: &idx,
|
||||||
|
used: &[],
|
||||||
|
};
|
||||||
|
match analyze_jpeg(&src, b_off, 4 << 20, &pool2, true) {
|
||||||
|
Analysis::Intact { len } => assert_eq!(len as usize, b.len()),
|
||||||
|
other => panic!("B deveria estar íntegro: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rebuilds_three_fragments_among_many_decoys() {
|
||||||
|
let a = photo_n(2048, 1536, 21, 10, 3);
|
||||||
|
let n = a.len();
|
||||||
|
let (s1, s2) = (10 * 4096, 10 * 4096 + (n - 10 * 4096) / 2 / 4096 * 4096);
|
||||||
|
assert!(n > s2 + 4 * 4096);
|
||||||
|
let mut disk: Vec<u8> = vec![0u8; 32 * 4096];
|
||||||
|
let a_off = disk.len() as u64;
|
||||||
|
disk.extend_from_slice(&a[..s1]);
|
||||||
|
let mut decoys = vec![];
|
||||||
|
for k in 0..6u64 {
|
||||||
|
let mut d = photo_n(1600, 1200, 100 + k, (k as i32) * 17, 3);
|
||||||
|
pad(&mut d);
|
||||||
|
decoys.push((disk.len() as u64, d.len() as u64));
|
||||||
|
disk.extend_from_slice(&d);
|
||||||
|
if k == 1 {
|
||||||
|
// segundo fragmento de A no meio
|
||||||
|
let off2 = disk.len() as u64;
|
||||||
|
disk.extend_from_slice(&a[s1..s2]);
|
||||||
|
decoys.push((off2, (s2 - s1) as u64));
|
||||||
|
}
|
||||||
|
if k == 4 {
|
||||||
|
let mut r = a[s2..].to_vec();
|
||||||
|
pad(&mut r);
|
||||||
|
disk.extend_from_slice(&r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let src = MemSource(disk.clone());
|
||||||
|
let mut idx = ClusterIndex::new(disk.len() as u64);
|
||||||
|
idx.scan_chunk(0, &disk);
|
||||||
|
eprintln!("TRUE frag2={:?}", decoys[2]);
|
||||||
|
let used: Vec<Extent> = decoys
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| *i != 2) // o fragmento do meio de A não é de outro arquivo
|
||||||
|
.map(|(_, (o, l))| Extent::new(*o, *l))
|
||||||
|
.collect();
|
||||||
|
let pool = Pool {
|
||||||
|
index: &idx,
|
||||||
|
used: &used,
|
||||||
|
};
|
||||||
|
match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) {
|
||||||
|
Analysis::Rebuilt { extents, size } => {
|
||||||
|
assert_eq!(size as usize, n);
|
||||||
|
let mut got = vec![];
|
||||||
|
for e in &extents {
|
||||||
|
got.extend_from_slice(&disk[e.offset as usize..(e.offset + e.len) as usize]);
|
||||||
|
}
|
||||||
|
assert!(got == a, "conteúdo difere ({} fragmentos)", extents.len());
|
||||||
|
}
|
||||||
|
other => panic!("{other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deep_scan_recovers_fragmented_jpeg_end_to_end() {
|
||||||
|
use guara_core::{recover, ScanMode, ScanOptions};
|
||||||
|
common::init();
|
||||||
|
let a = photo_n(2048, 1536, 5, 0, 3);
|
||||||
|
let n = a.len();
|
||||||
|
let split = 12 * 4096;
|
||||||
|
let mut disk: Vec<u8> = vec![0u8; 16 * 4096];
|
||||||
|
let a_off = disk.len();
|
||||||
|
disk.extend_from_slice(&a[..split]);
|
||||||
|
let mut b = photo_n(1600, 1200, 77, 30, 3);
|
||||||
|
pad(&mut b);
|
||||||
|
let b_len = b.len();
|
||||||
|
disk.extend_from_slice(&b);
|
||||||
|
disk.extend(noise(8 * 4096, 3));
|
||||||
|
let mut rest = a[split..].to_vec();
|
||||||
|
pad(&mut rest);
|
||||||
|
disk.extend_from_slice(&rest);
|
||||||
|
disk.extend(noise(8 * 4096, 4));
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let img = dir.path().join("d.img");
|
||||||
|
std::fs::write(&img, &disk).unwrap();
|
||||||
|
let c = common::scan_path(
|
||||||
|
&img,
|
||||||
|
&ScanOptions {
|
||||||
|
mode: ScanMode::Deep,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let 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<_>>()
|
||||||
|
);
|
||||||
|
let fa = jpgs
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.first_offset() == Some(a_off as u64))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(fa.size as usize, n, "{:?}", fa.note);
|
||||||
|
let out = dir.path().join("out");
|
||||||
|
let rep = common::recover_all(&img, std::slice::from_ref(*fa), &out);
|
||||||
|
let p = std::path::PathBuf::from(&rep.recovered[0].path);
|
||||||
|
let got = std::fs::read(if p.is_absolute() { p } else { out.join(p) }).unwrap();
|
||||||
|
assert!(got == a, "JPEG recuperado difere do original");
|
||||||
|
let fb = jpgs
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.first_offset() != Some(a_off as u64))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(fb.size as usize, photo_n(1600, 1200, 77, 30, 3).len());
|
||||||
|
let _ = (b_len, recover::RecoverReport::default());
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
//! Ponta a ponta: volume LUKS2 com ext4 dentro, arquivo apagado, senha informada ao Guará.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use guara_core::assemble::{assemble_source, Assembly, SecretInput};
|
||||||
|
use guara_core::scan::{self, Outcome};
|
||||||
|
use guara_core::source::{FileSource, Source};
|
||||||
|
use guara_core::*;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn have(t: &str) -> bool {
|
||||||
|
Command::new("sh")
|
||||||
|
.args(["-c", &format!("command -v {t}")])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_stdin(c: &mut Command, input: &str) -> bool {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut ch = c
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
ch.stdin
|
||||||
|
.take()
|
||||||
|
.unwrap()
|
||||||
|
.write_all(input.as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
ch.wait().unwrap().success()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(seed: u64, n: usize) -> Vec<u8> {
|
||||||
|
let mut x = seed | 1;
|
||||||
|
(0..n)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deleted_file_inside_luks2_is_recovered_with_password() {
|
||||||
|
if !(have("cryptsetup") && have("mke2fs") && have("debugfs") && have("python3")) {
|
||||||
|
eprintln!("SKIP: ferramentas ausentes");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
std::fs::create_dir_all(root.join("docs")).unwrap();
|
||||||
|
let secret_doc = data(11, 120_000);
|
||||||
|
std::fs::write(root.join("docs/segredo.dat"), &secret_doc).unwrap();
|
||||||
|
std::fs::write(root.join("docs/fica.dat"), data(12, 5000)).unwrap();
|
||||||
|
let plain = dir.path().join("plain.img");
|
||||||
|
std::fs::File::create(&plain)
|
||||||
|
.unwrap()
|
||||||
|
.set_len(8 << 20)
|
||||||
|
.unwrap();
|
||||||
|
assert!(Command::new("mke2fs")
|
||||||
|
.args(["-q", "-F", "-t", "ext4", "-d"])
|
||||||
|
.arg(&root)
|
||||||
|
.arg(&plain)
|
||||||
|
.status()
|
||||||
|
.unwrap()
|
||||||
|
.success());
|
||||||
|
let dbg = |c: &str| {
|
||||||
|
Command::new("debugfs")
|
||||||
|
.arg("-w")
|
||||||
|
.arg("-R")
|
||||||
|
.arg(c)
|
||||||
|
.arg(&plain)
|
||||||
|
.output()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
dbg("rm /docs/segredo.dat");
|
||||||
|
|
||||||
|
let img = dir.path().join("luks.img");
|
||||||
|
std::fs::File::create(&img)
|
||||||
|
.unwrap()
|
||||||
|
.set_len(32 << 20)
|
||||||
|
.unwrap();
|
||||||
|
let pw = "correct horse";
|
||||||
|
let mut fmt = Command::new("cryptsetup");
|
||||||
|
fmt.args([
|
||||||
|
"luksFormat",
|
||||||
|
"--batch-mode",
|
||||||
|
"--type",
|
||||||
|
"luks2",
|
||||||
|
"--pbkdf",
|
||||||
|
"pbkdf2",
|
||||||
|
"--pbkdf-force-iterations",
|
||||||
|
"1000",
|
||||||
|
"--sector-size",
|
||||||
|
"512",
|
||||||
|
"--key-file",
|
||||||
|
"-",
|
||||||
|
])
|
||||||
|
.arg(&img);
|
||||||
|
assert!(run_stdin(&mut fmt, pw));
|
||||||
|
let vkf = dir.path().join("vk");
|
||||||
|
let mut d = Command::new("cryptsetup");
|
||||||
|
d.args([
|
||||||
|
"luksDump",
|
||||||
|
"--dump-volume-key",
|
||||||
|
"--batch-mode",
|
||||||
|
"--key-file",
|
||||||
|
"-",
|
||||||
|
"--volume-key-file",
|
||||||
|
])
|
||||||
|
.arg(&vkf)
|
||||||
|
.arg(&img);
|
||||||
|
assert!(run_stdin(&mut d, pw));
|
||||||
|
let out = Command::new("cryptsetup")
|
||||||
|
.arg("luksDump")
|
||||||
|
.arg(&img)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let t = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
|
let off: u64 = t
|
||||||
|
.lines()
|
||||||
|
.skip_while(|l| !l.starts_with("Data segments"))
|
||||||
|
.find(|l| l.contains("offset:"))
|
||||||
|
.unwrap()
|
||||||
|
.split(':')
|
||||||
|
.nth(1)
|
||||||
|
.unwrap()
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
assert!(Command::new("python3")
|
||||||
|
.arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/support/luks_encrypt.py"))
|
||||||
|
.arg(&img)
|
||||||
|
.arg(&plain)
|
||||||
|
.arg(&vkf)
|
||||||
|
.arg(off.to_string())
|
||||||
|
.arg("aes-xts-plain64")
|
||||||
|
.arg("512")
|
||||||
|
.status()
|
||||||
|
.unwrap()
|
||||||
|
.success());
|
||||||
|
|
||||||
|
let base: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
|
||||||
|
// Sem senha: o volume segue fechado e a varredura não inventa arquivos.
|
||||||
|
let locked = assemble_source(base.clone(), &Assembly::default()).unwrap();
|
||||||
|
assert_eq!(locked.unlocked.len(), 0);
|
||||||
|
assert_eq!(locked.locked.len(), 1);
|
||||||
|
|
||||||
|
let asm = Assembly {
|
||||||
|
secrets: vec![SecretInput::Password { value: pw.into() }],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let open = assemble_source(base, &asm).unwrap();
|
||||||
|
assert_eq!(open.unlocked.len(), 1);
|
||||||
|
let mut sink = Collect::default();
|
||||||
|
let outc = scan::run(
|
||||||
|
open.source.as_ref(),
|
||||||
|
&ScanOptions {
|
||||||
|
mode: ScanMode::Deep,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
&[],
|
||||||
|
&|| false,
|
||||||
|
&mut sink,
|
||||||
|
);
|
||||||
|
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<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
assert_eq!(f.size, 120_000);
|
||||||
|
let dest = tempfile::tempdir().unwrap();
|
||||||
|
let refs = vec![f];
|
||||||
|
let rep = recover::recover(
|
||||||
|
open.source.as_ref(),
|
||||||
|
&refs,
|
||||||
|
dest.path(),
|
||||||
|
&|| false,
|
||||||
|
&mut |_, _, _| {},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(rep.failed.is_empty());
|
||||||
|
let p = &rep.recovered[0].path;
|
||||||
|
let p = if Path::new(p).is_absolute() {
|
||||||
|
Path::new(p).to_path_buf()
|
||||||
|
} else {
|
||||||
|
dest.path().join(p)
|
||||||
|
};
|
||||||
|
assert!(std::fs::read(p).unwrap() == secret_doc);
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
//! Ponta a ponta: RAID 5 de 4 discos (um perdido) com ext4 dentro e um arquivo apagado.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use guara_core::assemble::{assemble_source, Assembly};
|
||||||
|
use guara_core::scan::{self, Outcome};
|
||||||
|
use guara_core::source::{FileSource, Source};
|
||||||
|
use guara_core::*;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn have(t: &str) -> bool {
|
||||||
|
Command::new("sh")
|
||||||
|
.args(["-c", &format!("command -v {t}")])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(seed: u64, n: usize) -> Vec<u8> {
|
||||||
|
let mut x = seed | 1;
|
||||||
|
(0..n)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHUNK: usize = 64 * 1024;
|
||||||
|
|
||||||
|
fn md_super(n: u32, dev: u32, data_off: u64, sectors: u64) -> Vec<u8> {
|
||||||
|
let mut b = vec![0u8; 4096];
|
||||||
|
b[0..4].copy_from_slice(&0xA92B_4EFCu32.to_le_bytes());
|
||||||
|
b[4..8].copy_from_slice(&1u32.to_le_bytes());
|
||||||
|
b[16..32].fill(0x5a);
|
||||||
|
b[72..76].copy_from_slice(&5u32.to_le_bytes());
|
||||||
|
b[76..80].copy_from_slice(&2u32.to_le_bytes());
|
||||||
|
b[80..88].copy_from_slice(§ors.to_le_bytes());
|
||||||
|
b[88..92].copy_from_slice(&((CHUNK / 512) as u32).to_le_bytes());
|
||||||
|
b[92..96].copy_from_slice(&n.to_le_bytes());
|
||||||
|
b[128..136].copy_from_slice(&data_off.to_le_bytes());
|
||||||
|
b[136..144].copy_from_slice(§ors.to_le_bytes());
|
||||||
|
b[160..164].copy_from_slice(&dev.to_le_bytes());
|
||||||
|
b[200..208].copy_from_slice(&50u64.to_le_bytes());
|
||||||
|
b[220..224].copy_from_slice(&n.to_le_bytes());
|
||||||
|
for i in 0..n as usize {
|
||||||
|
b[256 + i * 2..258 + i * 2].copy_from_slice(&(i as u16).to_le_bytes());
|
||||||
|
}
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deleted_file_on_degraded_raid5() {
|
||||||
|
if !(have("mke2fs") && have("debugfs")) {
|
||||||
|
eprintln!("SKIP: e2fsprogs ausente");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
std::fs::create_dir_all(root.join("fotos")).unwrap();
|
||||||
|
let doc = data(5, 300_000);
|
||||||
|
std::fs::write(root.join("fotos/ferias.dat"), &doc).unwrap();
|
||||||
|
std::fs::write(root.join("fotos/outra.dat"), data(6, 9000)).unwrap();
|
||||||
|
let plain = dir.path().join("plain.img");
|
||||||
|
let n = 4usize;
|
||||||
|
let dd = n - 1;
|
||||||
|
let rows = 40usize;
|
||||||
|
let logical_len = rows * dd * CHUNK;
|
||||||
|
std::fs::File::create(&plain)
|
||||||
|
.unwrap()
|
||||||
|
.set_len(logical_len as u64)
|
||||||
|
.unwrap();
|
||||||
|
assert!(Command::new("mke2fs")
|
||||||
|
.args(["-q", "-F", "-t", "ext4", "-d"])
|
||||||
|
.arg(&root)
|
||||||
|
.arg(&plain)
|
||||||
|
.status()
|
||||||
|
.unwrap()
|
||||||
|
.success());
|
||||||
|
Command::new("debugfs")
|
||||||
|
.arg("-w")
|
||||||
|
.arg("-R")
|
||||||
|
.arg("rm /fotos/ferias.dat")
|
||||||
|
.arg(&plain)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let logical = std::fs::read(&plain).unwrap();
|
||||||
|
|
||||||
|
// RAID 5 left-symmetric
|
||||||
|
let data_off = 2048u64; // setores
|
||||||
|
let mut disks = vec![vec![0u8; data_off as usize * 512 + rows * CHUNK]; n];
|
||||||
|
for row in 0..rows {
|
||||||
|
let pd = n - 1 - row % n;
|
||||||
|
let mut p = vec![0u8; CHUNK];
|
||||||
|
for d in 0..dd {
|
||||||
|
let disk = (pd + 1 + d) % n;
|
||||||
|
let src = &logical[(row * dd + d) * CHUNK..(row * dd + d + 1) * CHUNK];
|
||||||
|
disks[disk][data_off as usize * 512 + row * CHUNK..][..CHUNK].copy_from_slice(src);
|
||||||
|
for i in 0..CHUNK {
|
||||||
|
p[i] ^= src[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disks[pd][data_off as usize * 512 + row * CHUNK..][..CHUNK].copy_from_slice(&p);
|
||||||
|
}
|
||||||
|
let mut paths = vec![];
|
||||||
|
for (i, d) in disks.iter_mut().enumerate() {
|
||||||
|
if i == 2 {
|
||||||
|
continue; // disco que morreu
|
||||||
|
}
|
||||||
|
let sb = md_super(n as u32, i as u32, data_off, (rows * CHUNK / 512) as u64);
|
||||||
|
d[4096..8192].copy_from_slice(&sb);
|
||||||
|
let p = dir.path().join(format!("disk{i}.img"));
|
||||||
|
std::fs::write(&p, &d).unwrap();
|
||||||
|
paths.push(p);
|
||||||
|
}
|
||||||
|
let base: Arc<dyn Source> = Arc::new(FileSource::open(&paths[0]).unwrap());
|
||||||
|
let asm = Assembly {
|
||||||
|
members: paths[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let a = assemble_source(base, &asm).unwrap();
|
||||||
|
assert!(
|
||||||
|
a.notes.iter().any(|n| n.contains("degradado")),
|
||||||
|
"{:?}",
|
||||||
|
a.notes
|
||||||
|
);
|
||||||
|
assert_eq!(a.extra_parts.len(), 1);
|
||||||
|
assert_eq!(a.extra_parts[0].fs, FsKind::Ext);
|
||||||
|
let opts = ScanOptions {
|
||||||
|
mode: ScanMode::Deep,
|
||||||
|
extra_parts: a.extra_parts.clone(),
|
||||||
|
covered: a.covered.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut sink = Collect::default();
|
||||||
|
let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink);
|
||||||
|
assert!(matches!(out, Outcome::Completed));
|
||||||
|
let f = sink
|
||||||
|
.found
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == "ferias.dat")
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"{:?}",
|
||||||
|
sink.found.iter().map(|f| &f.name).collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
assert_eq!(f.size, 300_000);
|
||||||
|
let dest = tempfile::tempdir().unwrap();
|
||||||
|
let rep = recover::recover(
|
||||||
|
a.source.as_ref(),
|
||||||
|
&[f],
|
||||||
|
dest.path(),
|
||||||
|
&|| false,
|
||||||
|
&mut |_, _, _| {},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
||||||
|
let p = &rep.recovered[0].path;
|
||||||
|
let p = if Path::new(p).is_absolute() {
|
||||||
|
Path::new(p).to_path_buf()
|
||||||
|
} else {
|
||||||
|
dest.path().join(p)
|
||||||
|
};
|
||||||
|
assert!(std::fs::read(p).unwrap() == doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raid5_without_metadata_is_detected_and_scanned() {
|
||||||
|
if !(have("mke2fs") && have("debugfs")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
for d in 0..10 {
|
||||||
|
std::fs::create_dir_all(root.join(format!("d{d}"))).unwrap();
|
||||||
|
for f in 0..30 {
|
||||||
|
std::fs::write(
|
||||||
|
root.join(format!("d{d}/f{f}.txt")),
|
||||||
|
vec![b'x'; 500 + f * 50],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let doc = data(9, 200_000);
|
||||||
|
std::fs::write(root.join("d3/perdido.dat"), &doc).unwrap();
|
||||||
|
let plain = dir.path().join("plain.img");
|
||||||
|
let (n, chunk) = (3usize, 64 * 1024usize);
|
||||||
|
let dd = n - 1;
|
||||||
|
let rows = 160usize;
|
||||||
|
std::fs::File::create(&plain)
|
||||||
|
.unwrap()
|
||||||
|
.set_len((rows * dd * chunk) as u64)
|
||||||
|
.unwrap();
|
||||||
|
assert!(Command::new("mke2fs")
|
||||||
|
.args(["-q", "-F", "-t", "ext4", "-d"])
|
||||||
|
.arg(&root)
|
||||||
|
.arg(&plain)
|
||||||
|
.status()
|
||||||
|
.unwrap()
|
||||||
|
.success());
|
||||||
|
Command::new("debugfs")
|
||||||
|
.arg("-w")
|
||||||
|
.arg("-R")
|
||||||
|
.arg("rm /d3/perdido.dat")
|
||||||
|
.arg(&plain)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let logical = std::fs::read(&plain).unwrap();
|
||||||
|
// left-asymmetric
|
||||||
|
let mut disks = vec![vec![0u8; rows * chunk]; n];
|
||||||
|
for row in 0..rows {
|
||||||
|
let pd = n - 1 - row % n;
|
||||||
|
let mut p = vec![0u8; chunk];
|
||||||
|
for (k, disk) in (0..n).filter(|&i| i != pd).enumerate() {
|
||||||
|
let s = &logical[(row * dd + k) * chunk..(row * dd + k + 1) * chunk];
|
||||||
|
disks[disk][row * chunk..(row + 1) * chunk].copy_from_slice(s);
|
||||||
|
for i in 0..chunk {
|
||||||
|
p[i] ^= s[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p);
|
||||||
|
}
|
||||||
|
let order = [1usize, 2, 0]; // fornecidos fora de ordem
|
||||||
|
let mut paths = vec![];
|
||||||
|
for (j, &i) in order.iter().enumerate() {
|
||||||
|
let p = dir.path().join(format!("m{j}.img"));
|
||||||
|
std::fs::write(&p, &disks[i]).unwrap();
|
||||||
|
paths.push(p);
|
||||||
|
}
|
||||||
|
let base: Arc<dyn Source> = Arc::new(FileSource::open(&paths[0]).unwrap());
|
||||||
|
let asm = Assembly {
|
||||||
|
members: paths[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.collect(),
|
||||||
|
raid: Some(guara_core::assemble::RaidSpec::default()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let a = assemble_source(base, &asm).unwrap();
|
||||||
|
assert!(a.skip_primary);
|
||||||
|
assert!(
|
||||||
|
a.notes.iter().any(|n| n.contains("descobertos")),
|
||||||
|
"{:?}",
|
||||||
|
a.notes
|
||||||
|
);
|
||||||
|
let opts = ScanOptions {
|
||||||
|
mode: ScanMode::Deep,
|
||||||
|
extra_parts: a.extra_parts.clone(),
|
||||||
|
skip_primary: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut sink = Collect::default();
|
||||||
|
let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink);
|
||||||
|
assert!(matches!(out, Outcome::Completed));
|
||||||
|
let f = sink
|
||||||
|
.found
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == "perdido.dat")
|
||||||
|
.expect("perdido.dat");
|
||||||
|
let dest = tempfile::tempdir().unwrap();
|
||||||
|
let rep = recover::recover(
|
||||||
|
a.source.as_ref(),
|
||||||
|
&[f],
|
||||||
|
dest.path(),
|
||||||
|
&|| false,
|
||||||
|
&mut |_, _, _| {},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let p = &rep.recovered[0].path;
|
||||||
|
let p = if Path::new(p).is_absolute() {
|
||||||
|
Path::new(p).to_path_buf()
|
||||||
|
} else {
|
||||||
|
dest.path().join(p)
|
||||||
|
};
|
||||||
|
assert!(std::fs::read(p).unwrap() == doc);
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use guara_core::ewf::{write_e01, E01Options, EwfSource};
|
||||||
|
use guara_core::source::{open_path, read_vec, Source};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn fx(name: &str) -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("tests/fixtures/ewf")
|
||||||
|
.join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw() -> Vec<u8> {
|
||||||
|
std::fs::read(fx("small.raw")).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reads_references_made_by_libewf() {
|
||||||
|
let want = raw();
|
||||||
|
for name in ["s_e6.E01", "s_e6n.E01", "s_x7.Ex01", "s_x7e.Ex01"] {
|
||||||
|
let s = open_path(&fx(name)).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||||
|
assert_eq!(s.size(), want.len() as u64, "{name}");
|
||||||
|
let got = read_vec(s.as_ref(), 0, want.len()).unwrap();
|
||||||
|
assert!(got == want, "{name}: conteúdo difere");
|
||||||
|
// leitura que cruza chunks e termina no fim
|
||||||
|
let tail = read_vec(s.as_ref(), want.len() as u64 - 5000, 9000).unwrap();
|
||||||
|
assert_eq!(tail, &want[want.len() - 5000..], "{name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stored_hash_matches() {
|
||||||
|
for name in ["s_e6.E01", "s_x7.Ex01"] {
|
||||||
|
let e = EwfSource::open(&fx(name)).unwrap();
|
||||||
|
assert_eq!(e.verify().unwrap(), Some(true), "{name}");
|
||||||
|
}
|
||||||
|
let e = EwfSource::open(&fx("s_e6.E01")).unwrap();
|
||||||
|
assert_eq!(e.info.version, 1);
|
||||||
|
assert_eq!(
|
||||||
|
e.info.fields.get("Examinador").map(String::as_str),
|
||||||
|
Some("examiner")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
e.info.fields.get("Número do caso").map(String::as_str),
|
||||||
|
Some("case1")
|
||||||
|
);
|
||||||
|
assert_eq!(EwfSource::open(&fx("s_x7.Ex01")).unwrap().info.version, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_then_read_roundtrip_with_segments() {
|
||||||
|
let mut x = 88172645463325252u64;
|
||||||
|
let data: Vec<u8> = (0..3_000_320u32)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let src = guara_core::source::MemSource(data.clone());
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let base = dir.path().join("img");
|
||||||
|
let opts = E01Options {
|
||||||
|
segment_size: 1_000_000,
|
||||||
|
case_number: "caso-1".into(),
|
||||||
|
examiner: "Lucas".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let rep = write_e01(&src, &base, &opts, &|| false, &mut |_, _| {}).unwrap();
|
||||||
|
assert!(
|
||||||
|
dir.path().join("img.E02").exists(),
|
||||||
|
"esperava mais de um segmento"
|
||||||
|
);
|
||||||
|
let r = open_path(&dir.path().join("img.E01")).unwrap();
|
||||||
|
assert_eq!(r.size(), data.len() as u64);
|
||||||
|
assert!(read_vec(r.as_ref(), 0, data.len()).unwrap() == data);
|
||||||
|
let e = EwfSource::open(&dir.path().join("img.E01")).unwrap();
|
||||||
|
assert_eq!(e.info.md5.as_deref(), Some(rep.md5.as_str()));
|
||||||
|
assert_eq!(e.verify().unwrap(), Some(true));
|
||||||
|
assert_eq!(
|
||||||
|
e.info.fields.get("Examinador").map(String::as_str),
|
||||||
|
Some("Lucas")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compressed_output_is_smaller_and_odd_sizes_work() {
|
||||||
|
let data = vec![7u8; 100_352];
|
||||||
|
let src = guara_core::source::MemSource(data.clone());
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let base = dir.path().join("z");
|
||||||
|
write_e01(
|
||||||
|
&src,
|
||||||
|
&base,
|
||||||
|
&E01Options {
|
||||||
|
segment_size: 0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&|| false,
|
||||||
|
&mut |_, _| {},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(std::fs::metadata(dir.path().join("z.E01")).unwrap().len() < 10_000);
|
||||||
|
let r = open_path(&dir.path().join("z.E01")).unwrap();
|
||||||
|
assert_eq!(r.size(), 100_352);
|
||||||
|
assert_eq!(read_vec(r.as_ref(), 0, 100_352).unwrap(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gera uma imagem para validar com ferramentas externas (ewfverify). `GUARA_E01_OUT=/caminho/base`.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn emit_for_external_verification() {
|
||||||
|
let Ok(base) = std::env::var("GUARA_E01_OUT") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let src = guara_core::source::MemSource(raw());
|
||||||
|
let opts = E01Options {
|
||||||
|
segment_size: 100_000,
|
||||||
|
case_number: "c1".into(),
|
||||||
|
examiner: "ex".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
write_e01(
|
||||||
|
&src,
|
||||||
|
std::path::Path::new(&base),
|
||||||
|
&opts,
|
||||||
|
&|| false,
|
||||||
|
&mut |_, _| {},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
//! ext4: arquivos apagados com nome, a partir de imagens criadas com mke2fs/debugfs.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use guara_core::*;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn have(tool: &str) -> bool {
|
||||||
|
Command::new("sh")
|
||||||
|
.args(["-c", &format!("command -v {tool}")])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debugfs(img: &Path, cmds: &[&str]) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for c in cmds {
|
||||||
|
let o = Command::new("debugfs")
|
||||||
|
.arg("-w")
|
||||||
|
.arg("-R")
|
||||||
|
.arg(c)
|
||||||
|
.arg(img)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
out.push_str(&String::from_utf8_lossy(&o.stdout));
|
||||||
|
out.push_str(&String::from_utf8_lossy(&o.stderr));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inode_of(img: &Path, path: &str) -> u64 {
|
||||||
|
let o = debugfs(img, &[&format!("stat {path}")]);
|
||||||
|
let l = o
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.starts_with("Inode:"))
|
||||||
|
.unwrap_or_else(|| panic!("{o}"));
|
||||||
|
l.split_whitespace().nth(1).unwrap().parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(seed: u32, len: usize) -> Vec<u8> {
|
||||||
|
let mut x = 88172645463325252u64 ^ seed as u64;
|
||||||
|
(0..len)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Img {
|
||||||
|
_dir: tempfile::TempDir,
|
||||||
|
img: PathBuf,
|
||||||
|
files: Vec<(&'static str, Vec<u8>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(opts: &[&str]) -> Option<Img> {
|
||||||
|
if !have("mke2fs") || !have("debugfs") {
|
||||||
|
eprintln!("SKIP: e2fsprogs ausente");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
std::fs::create_dir_all(root.join("docs/sub")).unwrap();
|
||||||
|
std::fs::create_dir_all(root.join("fotos")).unwrap();
|
||||||
|
let files = vec![
|
||||||
|
("docs/relatorio.dat", content(1, 150_000)),
|
||||||
|
("docs/sub/contrato.dat", content(2, 9_000)),
|
||||||
|
("fotos/viagem.dat", content(3, 700_000)),
|
||||||
|
("fotos/fica.dat", content(4, 30_000)),
|
||||||
|
];
|
||||||
|
for (p, c) in &files {
|
||||||
|
std::fs::write(root.join(p), c).unwrap();
|
||||||
|
}
|
||||||
|
let img = dir.path().join("e.img");
|
||||||
|
let f = std::fs::File::create(&img).unwrap();
|
||||||
|
f.set_len(24 << 20).unwrap();
|
||||||
|
let st = Command::new("mke2fs")
|
||||||
|
.args(["-q", "-F", "-d"])
|
||||||
|
.arg(&root)
|
||||||
|
.args(opts)
|
||||||
|
.arg(&img)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
st.status.success(),
|
||||||
|
"{}",
|
||||||
|
String::from_utf8_lossy(&st.stderr)
|
||||||
|
);
|
||||||
|
Some(Img {
|
||||||
|
_dir: dir,
|
||||||
|
img,
|
||||||
|
files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deep(img: &Path) -> common::Collect {
|
||||||
|
init();
|
||||||
|
let opts = ScanOptions {
|
||||||
|
mode: ScanMode::Deep,
|
||||||
|
..ScanOptions::default()
|
||||||
|
};
|
||||||
|
scan_path(img, &opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recovered(img: &Path, f: &Found) -> Vec<u8> {
|
||||||
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
let rep = recover_all(img, std::slice::from_ref(f), d.path());
|
||||||
|
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
||||||
|
let p = &rep.recovered[0].path;
|
||||||
|
let p = if Path::new(p).is_absolute() {
|
||||||
|
PathBuf::from(p)
|
||||||
|
} else {
|
||||||
|
d.path().join(p)
|
||||||
|
};
|
||||||
|
std::fs::read(p).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deleted_by_debugfs_keeps_extents_and_name() {
|
||||||
|
let Some(i) = build(&["-t", "ext4"]) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
debugfs(&i.img, &["rm /docs/relatorio.dat", "rm /fotos/viagem.dat"]);
|
||||||
|
let c = deep(&i.img);
|
||||||
|
for (p, data) in i
|
||||||
|
.files
|
||||||
|
.iter()
|
||||||
|
.filter(|f| f.0 == "docs/relatorio.dat" || f.0 == "fotos/viagem.dat")
|
||||||
|
{
|
||||||
|
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<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
assert_eq!(f.size, data.len() as u64);
|
||||||
|
assert_eq!(
|
||||||
|
f.dir.as_deref(),
|
||||||
|
Some(&format!("/{}", p.rsplit_once('/').unwrap().0)[..])
|
||||||
|
);
|
||||||
|
assert!(recovered(&i.img, f) == *data, "{name}");
|
||||||
|
}
|
||||||
|
// O arquivo que não foi apagado não aparece.
|
||||||
|
assert!(!c
|
||||||
|
.found
|
||||||
|
.iter()
|
||||||
|
.any(|f| f.name == "fica.dat" && f.origin == Origin::Ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncated_inode_recovered_by_remnant_extents() {
|
||||||
|
// Simula o que o kernel faz ao apagar: tamanho zerado e contagem de extents zerada.
|
||||||
|
let Some(i) = build(&["-t", "ext4"]) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let ino = inode_of(&i.img, "/docs/sub/contrato.dat");
|
||||||
|
debugfs(
|
||||||
|
&i.img,
|
||||||
|
&[
|
||||||
|
"rm /docs/sub/contrato.dat",
|
||||||
|
&format!("sif <{ino}> size 0"),
|
||||||
|
&format!("sif <{ino}> block[0] 0xF30A"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let c = deep(&i.img);
|
||||||
|
let f = c
|
||||||
|
.found
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == "contrato.dat")
|
||||||
|
.expect("contrato.dat");
|
||||||
|
assert_eq!(f.origin, Origin::Ext);
|
||||||
|
// Tamanho exato é perdido: recupera todos os blocos (9000 bytes arredondados para 3 blocos de 4K).
|
||||||
|
assert_eq!(f.size, 12288);
|
||||||
|
let got = recovered(&i.img, f);
|
||||||
|
assert_eq!(&got[..9000], &i.files[1].1[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn journal_copy_restores_exact_size() {
|
||||||
|
if !have("debugfs") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(i) = build(&["-t", "ext4", "-O", "has_journal"]) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Bloco da tabela de inodes que contém o inode do arquivo.
|
||||||
|
let ino = inode_of(&i.img, "/fotos/viagem.dat");
|
||||||
|
let o = debugfs(&i.img, &["stats"]);
|
||||||
|
let _ = o;
|
||||||
|
let blk_line = debugfs(&i.img, &[&format!("imap <{ino}>")]);
|
||||||
|
// "Inode 12 is part of block group 0\n\tlocated at block 1058, offset 0x0300"
|
||||||
|
let blk: u64 = blk_line
|
||||||
|
.split("located at block ")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split(',').next())
|
||||||
|
.and_then(|s| s.trim().parse().ok())
|
||||||
|
.unwrap_or_else(|| panic!("imap: {blk_line}"));
|
||||||
|
let bs = 4096u64;
|
||||||
|
let raw = std::fs::read(&i.img).unwrap();
|
||||||
|
let old = &raw[(blk * bs) as usize..((blk + 1) * bs) as usize];
|
||||||
|
let bin = i.img.with_extension("blk");
|
||||||
|
std::fs::write(&bin, old).unwrap();
|
||||||
|
let script = i.img.with_extension("cmds");
|
||||||
|
std::fs::write(
|
||||||
|
&script,
|
||||||
|
format!("jo\njw -b {blk} {}\njc\njc\n", bin.display()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let o = Command::new("debugfs")
|
||||||
|
.arg("-w")
|
||||||
|
.arg("-f")
|
||||||
|
.arg(&script)
|
||||||
|
.arg(&i.img)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let j = format!(
|
||||||
|
"{}{}",
|
||||||
|
String::from_utf8_lossy(&o.stdout),
|
||||||
|
String::from_utf8_lossy(&o.stderr)
|
||||||
|
);
|
||||||
|
if j.contains("Unknown request") || j.contains("not supported") {
|
||||||
|
eprintln!("SKIP: debugfs sem suporte a escrita no diário: {j}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Exclui e zera como o kernel faria.
|
||||||
|
debugfs(
|
||||||
|
&i.img,
|
||||||
|
&[
|
||||||
|
"rm /fotos/viagem.dat",
|
||||||
|
&format!("sif <{ino}> size 0"),
|
||||||
|
&format!("sif <{ino}> block[0] 0xF30A"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
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<_>>()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
assert_eq!(f.size, 700_000, "{:?}", f.note);
|
||||||
|
assert!(recovered(&i.img, f) == i.files[2].1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext2_blockmap_deleted_is_not_invented() {
|
||||||
|
// ext2 zera os ponteiros ao apagar; sem diário não há o que recuperar por metadados,
|
||||||
|
// mas a varredura não pode falhar nem listar o arquivo que continua existindo.
|
||||||
|
let Some(i) = build(&["-t", "ext2"]) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let c = deep(&i.img);
|
||||||
|
assert!(!c.found.iter().any(|f| f.origin == Origin::Ext));
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//! FileVault 2 (Core Storage): imagem de referência do cryptsetup (senha heslo123).
|
||||||
|
|
||||||
|
use guara_core::crypt::{filevault, Secret, UnlockError};
|
||||||
|
use guara_core::hashing::MultiHasher;
|
||||||
|
use guara_core::source::{FileSource, Source};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opens_reference_image_and_matches_known_hash() {
|
||||||
|
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../target/testdata/fvault2-images/small");
|
||||||
|
if !p.exists() {
|
||||||
|
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let src: Arc<dyn Source> = Arc::new(FileSource::open(&p).unwrap());
|
||||||
|
assert!(matches!(
|
||||||
|
filevault::unlock(src.clone(), &[Secret::Password("errada".into())]),
|
||||||
|
Err(UnlockError::WrongSecret)
|
||||||
|
));
|
||||||
|
let u = filevault::unlock(src, &[Secret::Password("heslo123".into())]).unwrap();
|
||||||
|
assert_eq!(u.source.size(), 167_772_160);
|
||||||
|
assert_eq!(
|
||||||
|
guara_core::crypt::to_hex(u.volume_key.as_ref().unwrap()),
|
||||||
|
"20734d3389212774d7610c29d732880 9".replace(' ', "")
|
||||||
|
);
|
||||||
|
let mut h = MultiHasher::new();
|
||||||
|
let mut off = 0;
|
||||||
|
let mut buf = vec![0u8; 1 << 20];
|
||||||
|
while off < u.source.size() {
|
||||||
|
let n = u.source.read_at(off, &mut buf).unwrap();
|
||||||
|
h.update(&buf[..n]);
|
||||||
|
off += n as u64;
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
h.finalize().sha256,
|
||||||
|
"2c662e36c0f7e2f5583e6a939bbcbdc660805692d0fccaa45ad4052beb3b8e18"
|
||||||
|
);
|
||||||
|
}
|
||||||