release: 1.1.0
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
This commit is contained in:
1 parent
5e43fa6d20
commit
b4a85722a6
28 files changed
+706
-160
No files matched your search
@@ -3,6 +3,19 @@ updates:
|
|||||||
- package-ecosystem: cargo
|
- package-ecosystem: cargo
|
||||||
directory: /
|
directory: /
|
||||||
schedule: { interval: weekly }
|
schedule: { interval: weekly }
|
||||||
|
groups:
|
||||||
|
rustcrypto:
|
||||||
|
patterns:
|
||||||
|
- sha2
|
||||||
|
- sha1
|
||||||
|
- md-5
|
||||||
|
- hmac
|
||||||
|
- pbkdf2
|
||||||
|
- ripemd
|
||||||
|
- aes
|
||||||
|
- argon2
|
||||||
|
- cipher
|
||||||
|
- digest
|
||||||
- package-ecosystem: cargo
|
- package-ecosystem: cargo
|
||||||
directory: /apps/gui/src-tauri
|
directory: /apps/gui/src-tauri
|
||||||
schedule: { interval: weekly }
|
schedule: { interval: weekly }
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- name: Cobertura da tradução da interface
|
||||||
|
run: node scripts/check-ui-i18n.mjs
|
||||||
- run: cargo fmt --all -- --check
|
- run: cargo fmt --all -- --check
|
||||||
- run: cargo clippy --workspace --all-targets
|
- run: cargo clippy --workspace --all-targets
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ jobs:
|
|||||||
id: notes
|
id: notes
|
||||||
run: |
|
run: |
|
||||||
v="${GITHUB_REF_NAME#v}"
|
v="${GITHUB_REF_NAME#v}"
|
||||||
|
v="${v%%-*}"
|
||||||
awk -v v="$v" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ {f=0} f' CHANGELOG.md > notes.md
|
awk -v v="$v" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ {f=0} f' CHANGELOG.md > notes.md
|
||||||
[ -s notes.md ] || echo "Veja o CHANGELOG." > notes.md
|
[ -s notes.md ] || echo "Veja o CHANGELOG." > notes.md
|
||||||
- uses: actions/github-script@v9
|
- uses: actions/github-script@v9
|
||||||
@@ -112,7 +113,7 @@ jobs:
|
|||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
- name: Gera os ícones
|
- name: Gera os ícones
|
||||||
run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons
|
run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons
|
||||||
- uses: tauri-apps/tauri-action@v0
|
- uses: tauri-apps/tauri-action@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -125,10 +126,21 @@ jobs:
|
|||||||
needs: [create-release, cli, gui]
|
needs: [create-release, cli, gui]
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Notas da versão (do CHANGELOG)
|
||||||
|
run: |
|
||||||
|
v="${GITHUB_REF_NAME#v}"
|
||||||
|
v="${v%%-*}"
|
||||||
|
awk -v v="$v" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ {f=0} f' CHANGELOG.md > notes.md
|
||||||
|
[ -s notes.md ] || echo "Veja o CHANGELOG." > notes.md
|
||||||
- uses: actions/github-script@v9
|
- uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
await github.rest.repos.updateRelease({
|
await github.rest.repos.updateRelease({
|
||||||
owner: context.repo.owner, repo: context.repo.repo,
|
owner: context.repo.owner, repo: context.repo.repo,
|
||||||
release_id: ${{ needs.create-release.outputs.release_id }}, draft: false,
|
release_id: ${{ needs.create-release.outputs.release_id }},
|
||||||
|
name: `Guará ${context.ref.replace('refs/tags/', '')}`,
|
||||||
|
body: fs.readFileSync('notes.md', 'utf8'),
|
||||||
|
draft: false,
|
||||||
});
|
});
|
||||||
@@ -4,6 +4,29 @@
|
|||||||
|
|
||||||
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
||||||
|
|
||||||
|
## [1.1.0] - 2026-10-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Automated translation check** (`scripts/check-ui-i18n.mjs`): scans the interface JS, HTML and the labels emitted by the Rust core, fails on Portuguese text without an English entry and on duplicate dictionary keys. It runs in the CI lint job.
|
||||||
|
- **Dependabot group for RustCrypto:** `sha2`, `sha1`, `md-5`, `hmac`, `pbkdf2`, `ripemd`, `aes`, `argon2`, `cipher` and `digest` now arrive in one update, because their traits are only compatible when the versions move together.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **English interface for core messages:** the most common errors from the core now have `{n}` patterns in the dictionary, so paths and sizes are translated too.
|
||||||
|
- **Notifications in the chosen language:** the core formats scan/recovery notifications using the saved interface language (`ui_lang` follows `lang` or the system locale when unset).
|
||||||
|
- **ISO 8601 dates:** EXIF, PDF and MP4 metadata dates are emitted as `YYYY-MM-DDTHH:MM:SS` (MP4 with `Z`) and formatted by the interface in the selected locale, instead of a pre-formatted Brazilian date.
|
||||||
|
- **RustCrypto on digest 0.11 / cipher 0.5:** `aes` 0.9, `sha2` 0.11, `sha1` 0.11, `md-5` 0.11, `hmac` 0.13, `pbkdf2` 0.13, `ripemd` 0.2 and `argon2` 0.6. BitLocker, LUKS and FileVault 2 were checked against the reference images.
|
||||||
|
- **Release workflow:** `tauri-action` updated to v1; the publish job restores the release name and the CHANGELOG notes after the desktop builds, so v1 cannot overwrite them.
|
||||||
|
- **Docker documentation:** `--device=...:r` was verified with a loop device (`inspect`, `scan`, `recover` read; writes are refused). The SMART/`SYS_RAWIO` wording now states what was and was not verified.
|
||||||
|
- **Superset workspace:** setup, Run and teardown were verified in real workspaces; two interfaces get different ports, and deleting a workspace stops its `guara ui`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Eight duplicated keys in `apps/gui/ui/i18n-en.js` (the last one won silently).
|
||||||
|
- Untranslated fragments split by inline tags, metadata labels and preview messages that the previous manual pass missed.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- No change to scanning or recovery logic. The encrypted-volume tests pass with `GUARA_REQUIRE_FIXTURES=1`.
|
||||||
|
- CLI output remains in Portuguese.
|
||||||
|
|
||||||
## [1.0.1] - 2026-10-20
|
## [1.0.1] - 2026-10-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -4,6 +4,29 @@
|
|||||||
|
|
||||||
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.1.0] - 2026-10-20
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- **Verificação automática da tradução** (`scripts/check-ui-i18n.mjs`): varre o JS e o HTML da interface e os rótulos emitidos pelo núcleo em Rust, falha quando há texto em português sem entrada em inglês e quando há chave duplicada no dicionário. Roda no job de lint do CI.
|
||||||
|
- **Grupo RustCrypto no Dependabot:** `sha2`, `sha1`, `md-5`, `hmac`, `pbkdf2`, `ripemd`, `aes`, `argon2`, `cipher` e `digest` passam a chegar numa única atualização, porque as traits só são compatíveis quando as versões andam juntas.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- **Interface em inglês para mensagens do núcleo:** os erros mais comuns do núcleo agora têm padrões com `{n}` no dicionário, então caminhos e tamanhos também são traduzidos.
|
||||||
|
- **Notificações no idioma escolhido:** o núcleo monta as notificações de varredura e recuperação usando o idioma salvo da interface (`ui_lang` segue `lang` ou o locale do sistema quando não definido).
|
||||||
|
- **Datas em ISO 8601:** as datas de metadados EXIF, PDF e MP4 saem como `YYYY-MM-DDTHH:MM:SS` (MP4 com `Z`) e são formatadas pela interface no locale escolhido, em vez de uma data brasileira já pronta.
|
||||||
|
- **RustCrypto em digest 0.11 / cipher 0.5:** `aes` 0.9, `sha2` 0.11, `sha1` 0.11, `md-5` 0.11, `hmac` 0.13, `pbkdf2` 0.13, `ripemd` 0.2 e `argon2` 0.6. BitLocker, LUKS e FileVault 2 foram conferidos contra as imagens de referência.
|
||||||
|
- **Fluxo de release:** `tauri-action` atualizado para v1; o job de publicação restaura o nome e as notas do CHANGELOG depois dos builds do desktop, para a v1 não sobrescrevê-las.
|
||||||
|
- **Documentação do Docker:** `--device=...:r` foi verificado com um dispositivo de loop (`inspect`, `scan`, `recover` leem; escritas são recusadas). O texto sobre SMART/`SYS_RAWIO` agora diz o que foi e o que não foi verificado.
|
||||||
|
- **Workspace do Superset:** setup, Run e teardown foram verificados em workspaces reais; duas interfaces recebem portas diferentes, e apagar um workspace encerra o `guara ui`.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
- Oito chaves duplicadas em `apps/gui/ui/i18n-en.js` (a última vencia em silêncio).
|
||||||
|
- Trechos sem tradução separados por tags inline, rótulos de metadados e mensagens da pré-visualização que a passada manual anterior não pegou.
|
||||||
|
|
||||||
|
### Notas
|
||||||
|
- Nenhuma mudança na lógica de varredura ou recuperação. Os testes de volumes cifrados passam com `GUARA_REQUIRE_FIXTURES=1`.
|
||||||
|
- A saída do terminal continua em português.
|
||||||
|
|
||||||
## [1.0.1] - 2026-10-20
|
## [1.0.1] - 2026-10-20
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|||||||
Generated
+95
-77
@@ -10,12 +10,12 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aes"
|
name = "aes"
|
||||||
version = "0.8.4"
|
version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"cipher",
|
"cipher",
|
||||||
|
"cpubits",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -71,9 +71,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2"
|
name = "argon2"
|
||||||
version = "0.5.3"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64ct",
|
"base64ct",
|
||||||
"blake2",
|
"blake2",
|
||||||
@@ -107,20 +107,20 @@ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blake2"
|
name = "blake2"
|
||||||
version = "0.10.6"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -191,9 +191,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
"inout",
|
"inout",
|
||||||
@@ -239,6 +239,12 @@ version = "1.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmov"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -252,31 +258,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "const-oid"
|
||||||
version = "0.2.17"
|
version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpubits"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.2"
|
version = "1.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"hybrid-array",
|
||||||
"typenum",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -291,14 +308,24 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "ctutils"
|
||||||
version = "0.10.7"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
|
||||||
|
dependencies = [
|
||||||
|
"cmov",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
|
"const-oid",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
"subtle",
|
"ctutils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -355,16 +382,6 @@ dependencies = [
|
|||||||
"zlib-rs",
|
"zlib-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "generic-array"
|
|
||||||
version = "0.14.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|
||||||
dependencies = [
|
|
||||||
"typenum",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -388,7 +405,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "guara-cli"
|
name = "guara-cli"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
@@ -401,7 +418,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "guara-core"
|
name = "guara-core"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -444,9 +461,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.12.1"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
@@ -457,6 +474,15 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hybrid-array"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.10"
|
version = "0.25.10"
|
||||||
@@ -495,11 +521,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.4"
|
version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
checksum = "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -558,9 +584,9 @@ checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"digest",
|
"digest",
|
||||||
@@ -697,25 +723,35 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "password-hash"
|
name = "password-hash"
|
||||||
version = "0.5.0"
|
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 = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64ct",
|
"getrandom",
|
||||||
"rand_core",
|
"phc",
|
||||||
"subtle",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbkdf2"
|
name = "pbkdf2"
|
||||||
version = "0.12.2"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"hmac",
|
"hmac",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phc"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"ctutils",
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -765,17 +801,11 @@ 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]]
|
[[package]]
|
||||||
name = "ripemd"
|
name = "ripemd"
|
||||||
version = "0.1.3"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
|
checksum = "4dd4211456b4172d7e44261920c25acf07367c4f04bb5f5d54fc21b090d9b159"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
@@ -868,9 +898,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.7"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
|
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
@@ -879,9 +909,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
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 = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
@@ -906,12 +936,6 @@ 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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.119"
|
version = "2.0.119"
|
||||||
@@ -1007,9 +1031,9 @@ checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typed-path"
|
name = "typed-path"
|
||||||
version = "0.12.3"
|
version = "0.12.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
checksum = "54ec2755506471f54baf5e4bb5499d8e510cc1caf271270e778a736bd2d7336c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
@@ -1029,12 +1053,6 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.9.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
|||||||
+1
-1
@@ -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 = "1.0.1"
|
version = "1.1.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"
|
||||||
|
|||||||
+2
-1
@@ -25,10 +25,11 @@ Legend: ✅ done · 🟡 partial · 🔜 planned · 🔭 long-term idea · ⛔ o
|
|||||||
| 0.6.0 | 2026-10-18 | Full disk health (ATA and NVMe SMART), `guara health`, new logo |
|
| 0.6.0 | 2026-10-18 | Full disk health (ATA and NVMe SMART), `guara health`, new logo |
|
||||||
| 1.0.0 | 2026-10-19 | ext4, HFS+, Btrfs, XFS, APFS; BitLocker, LUKS, FileVault 2; RAID and LVM; E01, Ex01, AFF4; fragmented JPEG; MP4 without moov |
|
| 1.0.0 | 2026-10-19 | ext4, HFS+, Btrfs, XFS, APFS; BitLocker, LUKS, FileVault 2; RAID and LVM; E01, Ex01, AFF4; fragmented JPEG; MP4 without moov |
|
||||||
| 1.0.1 | 2026-10-20 | Documentation in English with Portuguese translations, complete English interface, screenshots, Docker image of the CLI |
|
| 1.0.1 | 2026-10-20 | Documentation in English with Portuguese translations, complete English interface, screenshots, Docker image of the CLI |
|
||||||
|
| 1.1.0 | 2026-10-20 | Core messages and dates in English, automated translation check, RustCrypto on digest 0.11, verified Docker device access and Superset workspaces |
|
||||||
|
|
||||||
Details of each version in the [CHANGELOG](CHANGELOG.md).
|
Details of each version in the [CHANGELOG](CHANGELOG.md).
|
||||||
|
|
||||||
## Current status (1.0.1) against what is expected from a forensic suite
|
## Current status (1.1.0) against what is expected from a forensic suite
|
||||||
|
|
||||||
### 1. File systems with metadata
|
### 1. File systems with metadata
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -25,10 +25,11 @@ Legenda: ✅ feito · 🟡 parcial · 🔜 planejado · 🔭 ideia a longo prazo
|
|||||||
| 0.6.0 | 2026-10-18 | Saúde do disco completa (SMART ATA e NVMe), `guara health`, nova logo |
|
| 0.6.0 | 2026-10-18 | Saúde do disco completa (SMART ATA e NVMe), `guara health`, nova logo |
|
||||||
| 1.0.0 | 2026-10-19 | ext4, HFS+, Btrfs, XFS, APFS; BitLocker, LUKS, FileVault 2; RAID e LVM; E01, Ex01, AFF4; JPEG fragmentado; MP4 sem moov |
|
| 1.0.0 | 2026-10-19 | ext4, HFS+, Btrfs, XFS, APFS; BitLocker, LUKS, FileVault 2; RAID e LVM; E01, Ex01, AFF4; JPEG fragmentado; MP4 sem moov |
|
||||||
| 1.0.1 | 2026-10-20 | Documentação em inglês com tradução em português, interface completa em inglês, telas, imagem Docker do terminal |
|
| 1.0.1 | 2026-10-20 | Documentação em inglês com tradução em português, interface completa em inglês, telas, imagem Docker do terminal |
|
||||||
|
| 1.1.0 | 2026-10-20 | Mensagens do núcleo e datas em inglês, verificação automática da tradução, RustCrypto em digest 0.11, acesso a dispositivo no Docker e workspaces do Superset verificados |
|
||||||
|
|
||||||
Detalhes de cada versão no [CHANGELOG](CHANGELOG.pt-BR.md).
|
Detalhes de cada versão no [CHANGELOG](CHANGELOG.pt-BR.md).
|
||||||
|
|
||||||
## Situação atual (1.0.1) contra o que se espera de uma suíte forense
|
## Situação atual (1.1.0) contra o que se espera de uma suíte forense
|
||||||
|
|
||||||
### 1. Sistemas de arquivos com metadados
|
### 1. Sistemas de arquivos com metadados
|
||||||
|
|
||||||
|
|||||||
@@ -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 = "1.0.1"
|
version = "1.1.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"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Guara",
|
"productName": "Guara",
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"identifier": "dev.guara.app",
|
"identifier": "dev.guara.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../ui"
|
"frontendDist": "../ui"
|
||||||
|
|||||||
+5
-3
@@ -2,7 +2,7 @@
|
|||||||
// • app Tauri: invoke('rpc') • modo `guara ui`: POST /rpc/<método> com token.
|
// • app Tauri: invoke('rpc') • modo `guara ui`: POST /rpc/<método> com token.
|
||||||
|
|
||||||
import { createExplorer } from './explorer.js';
|
import { createExplorer } from './explorer.js';
|
||||||
import { setLang, LOC } from './i18n.js';
|
import { setLang, LOC, currentLang } from './i18n.js';
|
||||||
|
|
||||||
const tauri = window.__TAURI__;
|
const tauri = window.__TAURI__;
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
@@ -53,7 +53,7 @@ document.querySelectorAll('[data-theme-pick]').forEach((b) => (b.onclick = async
|
|||||||
|
|
||||||
// ------------------------------------------------------------------ configurações
|
// ------------------------------------------------------------------ configurações
|
||||||
// (declarado antes do uso: `S` é definido mais abaixo, por isso o objeto fica aqui)
|
// (declarado antes do uso: `S` é definido mais abaixo, por isso o objeto fica aqui)
|
||||||
const SETTINGS = { theme: 'auto', lang: 'auto', notify: true, maps: false };
|
const SETTINGS = { theme: 'auto', lang: 'auto', ui_lang: null, notify: true, maps: false };
|
||||||
async function saveSettings(patch) {
|
async function saveSettings(patch) {
|
||||||
Object.assign(SETTINGS, patch);
|
Object.assign(SETTINGS, patch);
|
||||||
try { await rpc('settings_set', SETTINGS); } catch { /* segue só com o armazenamento local */ }
|
try { await rpc('settings_set', SETTINGS); } catch { /* segue só com o armazenamento local */ }
|
||||||
@@ -750,8 +750,9 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
|||||||
d.onclose = () => {
|
d.onclose = () => {
|
||||||
if (d.returnValue !== 'ok') return;
|
if (d.returnValue !== 'ok') return;
|
||||||
const patch = { lang: d.querySelector('[name=lang]').value, notify: d.querySelector('[name=notify]').checked, maps: d.querySelector('[name=maps]').checked };
|
const patch = { lang: d.querySelector('[name=lang]').value, notify: d.querySelector('[name=notify]').checked, maps: d.querySelector('[name=maps]').checked };
|
||||||
saveSettings(patch);
|
|
||||||
setLang(patch.lang);
|
setLang(patch.lang);
|
||||||
|
patch.ui_lang = currentLang();
|
||||||
|
saveSettings(patch);
|
||||||
EX?.relang?.();
|
EX?.relang?.();
|
||||||
toast('Salvo');
|
toast('Salvo');
|
||||||
};
|
};
|
||||||
@@ -804,6 +805,7 @@ function watchClose() {
|
|||||||
try { localStorage.setItem('guara-theme', st.theme); } catch { /* */ }
|
try { localStorage.setItem('guara-theme', st.theme); } catch { /* */ }
|
||||||
} catch { /* usa o local */ }
|
} catch { /* usa o local */ }
|
||||||
setLang(SETTINGS.lang);
|
setLang(SETTINGS.lang);
|
||||||
|
if (SETTINGS.ui_lang !== currentLang()) saveSettings({ ui_lang: currentLang() });
|
||||||
watchClose();
|
watchClose();
|
||||||
tauri?.event?.listen?.('elevate-failed', (e) => { toast(String(e.payload || 'Não foi possível reiniciar como administrador')); });
|
tauri?.event?.listen?.('elevate-failed', (e) => { toast(String(e.payload || 'Não foi possível reiniciar como administrador')); });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ export function createExplorer(deps) {
|
|||||||
: new Date(k + '-01T00:00:00Z').toLocaleDateString(LOC(), { timeZone: 'UTC', month: 'long', year: 'numeric' });
|
: new Date(k + '-01T00:00:00Z').toLocaleDateString(LOC(), { timeZone: 'UTC', month: 'long', year: 'numeric' });
|
||||||
};
|
};
|
||||||
const fmtDate = (t) => (t == null ? 'sem data' : new Date(t * 1000).toLocaleString(LOC(), { timeZone: 'UTC', dateStyle: 'medium', timeStyle: 'short' }));
|
const fmtDate = (t) => (t == null ? 'sem data' : new Date(t * 1000).toLocaleString(LOC(), { timeZone: 'UTC', dateStyle: 'medium', timeStyle: 'short' }));
|
||||||
|
// Metadados do núcleo trazem datas em ISO 8601; o idioma decide a ordem.
|
||||||
|
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
|
||||||
|
const fmtMeta = (v) => ISO_DATE.test(v) ? new Date(v.endsWith('Z') ? v : v + 'Z').toLocaleString(LOC(), { timeZone: 'UTC', dateStyle: 'medium', timeStyle: 'medium' }) : v;
|
||||||
const fmtDay = (t) => (t == null ? '' : new Date(t * 1000).toLocaleDateString(LOC(), { timeZone: 'UTC', day: '2-digit', month: 'short', year: 'numeric' }));
|
const fmtDay = (t) => (t == null ? '' : new Date(t * 1000).toLocaleDateString(LOC(), { timeZone: 'UTC', day: '2-digit', month: 'short', year: 'numeric' }));
|
||||||
|
|
||||||
function geometry() {
|
function geometry() {
|
||||||
@@ -591,7 +594,7 @@ export function createExplorer(deps) {
|
|||||||
if (my !== metaToken || !$('#xpMeta')) return;
|
if (my !== metaToken || !$('#xpMeta')) return;
|
||||||
const rows = (m.fields || []).filter(([k]) => !['Dimensões'].includes(k));
|
const rows = (m.fields || []).filter(([k]) => !['Dimensões'].includes(k));
|
||||||
if (!rows.length) return;
|
if (!rows.length) return;
|
||||||
$('#xpMeta').innerHTML = `<h5>Metadados${m.kind ? ` · ${esc(m.kind)}` : ''}</h5><dl class="kv">${rows.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join('')}</dl>`
|
$('#xpMeta').innerHTML = `<h5>Metadados${m.kind ? ` · ${esc(m.kind)}` : ''}</h5><dl class="kv">${rows.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(fmtMeta(v))}</dd>`).join('')}</dl>`
|
||||||
+ (m.gps ? gpsHtml(m.gps) : '');
|
+ (m.gps ? gpsHtml(m.gps) : '');
|
||||||
$('#xpOsm')?.addEventListener('click', () => rpc('open_url', { url: `https://www.openstreetmap.org/?mlat=${m.gps[0]}&mlon=${m.gps[1]}#map=15/${m.gps[0]}/${m.gps[1]}` }).catch(() => {}));
|
$('#xpOsm')?.addEventListener('click', () => rpc('open_url', { url: `https://www.openstreetmap.org/?mlat=${m.gps[0]}&mlon=${m.gps[1]}#map=15/${m.gps[0]}/${m.gps[1]}` }).catch(() => {}));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|||||||
+80
-10
@@ -1,7 +1,6 @@
|
|||||||
// Dicionário português -> inglês da interface. {n} marca um valor variável.
|
// Dicionário português -> inglês da interface. {n} marca um valor variável.
|
||||||
export const EN = {
|
export const EN = {
|
||||||
// saúde do disco (SMART)
|
// saúde do disco (SMART)
|
||||||
'Saúde do disco': 'Disk health',
|
|
||||||
'O SMART não pôde ser lido. Instale o pacote smartmontools (comando smartctl) e rode como administrador para ver a saúde do disco.': 'SMART could not be read. Install the smartmontools package (smartctl command) and run as administrator to see the disk health.',
|
'O SMART não pôde ser lido. Instale o pacote smartmontools (comando smartctl) e rode como administrador para ver a saúde do disco.': 'SMART could not be read. Install the smartmontools package (smartctl command) and run as administrator to see the disk health.',
|
||||||
'Setores realocados': 'Reallocated sectors',
|
'Setores realocados': 'Reallocated sectors',
|
||||||
'Setores pendentes de realocação': 'Pending sectors',
|
'Setores pendentes de realocação': 'Pending sectors',
|
||||||
@@ -13,7 +12,6 @@ export const EN = {
|
|||||||
'Erros de mídia': 'Media errors',
|
'Erros de mídia': 'Media errors',
|
||||||
'Reserva de blocos abaixo do limite': 'Spare blocks below threshold',
|
'Reserva de blocos abaixo do limite': 'Spare blocks below threshold',
|
||||||
'Erros de leitura/gravação vistos pelo sistema': 'Read/write errors seen by the system',
|
'Erros de leitura/gravação vistos pelo sistema': 'Read/write errors seen by the system',
|
||||||
'O sistema já registrou erros de E/S neste disco.': 'The system has already logged I/O errors on this disk.',
|
|
||||||
'Estado': 'Status',
|
'Estado': 'Status',
|
||||||
'Bom': 'Good',
|
'Bom': 'Good',
|
||||||
'Atenção': 'Caution',
|
'Atenção': 'Caution',
|
||||||
@@ -40,9 +38,6 @@ export const EN = {
|
|||||||
'O SMART é do disco inteiro: os dados abaixo são de': 'SMART belongs to the whole disk: the data below is for',
|
'O SMART é do disco inteiro: os dados abaixo são de': 'SMART belongs to the whole disk: the data below is for',
|
||||||
', que contém': ', which contains',
|
', que contém': ', which contains',
|
||||||
'Imagem de disco: não há SMART para avaliar.': 'Disk image: there is no SMART to check.',
|
'Imagem de disco: não há SMART para avaliar.': 'Disk image: there is no SMART to check.',
|
||||||
'Nenhum sinal de defeito encontrado pelo SMART.': 'SMART found no signs of failure.',
|
|
||||||
'Este disco mostra sinais sérios de defeito.': 'This disk shows serious signs of failure.',
|
|
||||||
'Este disco mostra sinais de desgaste ou erros.': 'This disk shows signs of wear or errors.',
|
|
||||||
// barra lateral e estrutura
|
// barra lateral e estrutura
|
||||||
'Recuperação de arquivos': 'File recovery',
|
'Recuperação de arquivos': 'File recovery',
|
||||||
'Dispositivos': 'Devices',
|
'Dispositivos': 'Devices',
|
||||||
@@ -50,9 +45,7 @@ export const EN = {
|
|||||||
'Atualizar lista': 'Refresh list',
|
'Atualizar lista': 'Refresh list',
|
||||||
'Lista atualizada': 'List refreshed',
|
'Lista atualizada': 'List refreshed',
|
||||||
'Abrir arquivo de imagem…': 'Open disk image file…',
|
'Abrir arquivo de imagem…': 'Open disk image file…',
|
||||||
'Abrir arquivo de imagem': 'Open disk image file',
|
|
||||||
'Abrir sessão salva…': 'Open saved session…',
|
'Abrir sessão salva…': 'Open saved session…',
|
||||||
'Abrir sessão salva': 'Open saved session',
|
|
||||||
'Sessões': 'Sessions',
|
'Sessões': 'Sessions',
|
||||||
'Sessão': 'Session',
|
'Sessão': 'Session',
|
||||||
'Sessão:': 'Session:',
|
'Sessão:': 'Session:',
|
||||||
@@ -181,7 +174,6 @@ export const EN = {
|
|||||||
'Por assinatura': 'By signature',
|
'Por assinatura': 'By signature',
|
||||||
'Sem data': 'No date',
|
'Sem data': 'No date',
|
||||||
'sem data': 'no date',
|
'sem data': 'no date',
|
||||||
'Confiança {n}': 'Confidence {n}',
|
|
||||||
'Com localização (GPS)': 'With location (GPS)',
|
'Com localização (GPS)': 'With location (GPS)',
|
||||||
'Com GPS': 'With GPS',
|
'Com GPS': 'With GPS',
|
||||||
'Ocultar duplicadas': 'Hide duplicates',
|
'Ocultar duplicadas': 'Hide duplicates',
|
||||||
@@ -308,7 +300,6 @@ export const EN = {
|
|||||||
// recuperar
|
// recuperar
|
||||||
'Recuperar para…': 'Recover to…',
|
'Recuperar para…': 'Recover to…',
|
||||||
'Recuperar': 'Recover',
|
'Recuperar': 'Recover',
|
||||||
'Escolha uma pasta em <b>outro disco</b>. O Guará nunca grava no disco de origem, para não sobrescrever o que ainda dá para salvar.': 'Choose a folder on <b>another disk</b>. Guará never writes to the source disk, so it does not overwrite what can still be saved.',
|
|
||||||
'Pasta de destino': 'Destination folder',
|
'Pasta de destino': 'Destination folder',
|
||||||
'Escolher…': 'Choose…',
|
'Escolher…': 'Choose…',
|
||||||
'Organizar os arquivos': 'Organize the files',
|
'Organizar os arquivos': 'Organize the files',
|
||||||
@@ -346,7 +337,6 @@ export const EN = {
|
|||||||
'Se a recuperação for interrompida, escolha a mesma pasta de novo: o Guará pula o que já foi conferido.': 'If the recovery is interrupted, pick the same folder again: Guará skips what was already verified.',
|
'Se a recuperação for interrompida, escolha a mesma pasta de novo: o Guará pula o que já foi conferido.': 'If the recovery is interrupted, pick the same folder again: Guará skips what was already verified.',
|
||||||
'{n} já estavam recuperados e conferidos (pulados).': '{n} were already recovered and verified (skipped).',
|
'{n} já estavam recuperados e conferidos (pulados).': '{n} were already recovered and verified (skipped).',
|
||||||
// imagem, hash, busca
|
// imagem, hash, busca
|
||||||
'Onde salvar o arquivo de imagem? Use <b>outro disco</b> com espaço suficiente e um nome que ainda não exista.': 'Where to save the image file? Use <b>another disk</b> with enough space and a name that does not exist yet.',
|
|
||||||
'Criando imagem do disco': 'Creating disk image',
|
'Criando imagem do disco': 'Creating disk image',
|
||||||
'Nenhum setor ruim encontrado.': 'No bad sectors found.',
|
'Nenhum setor ruim encontrado.': 'No bad sectors found.',
|
||||||
'Calculando hashes da origem': 'Computing source hashes',
|
'Calculando hashes da origem': 'Computing source hashes',
|
||||||
@@ -505,4 +495,84 @@ export const EN = {
|
|||||||
'Tabela virtual: o conteúdo não fica armazenado como linhas comuns.': 'Virtual table: the content is not stored as ordinary rows.',
|
'Tabela virtual: o conteúdo não fica armazenado como linhas comuns.': 'Virtual table: the content is not stored as ordinary rows.',
|
||||||
'Tabela WITHOUT ROWID: as linhas ficam numa árvore de índice, ainda não exibida.': 'WITHOUT ROWID table: the rows live in an index tree, not shown yet.',
|
'Tabela WITHOUT ROWID: as linhas ficam numa árvore de índice, ainda não exibida.': 'WITHOUT ROWID table: the rows live in an index tree, not shown yet.',
|
||||||
'Arquivo truncado: o cabeçalho indica {n} páginas, mas só há {n}. Parte dos dados pode estar faltando.': 'Truncated file: the header says {n} pages, but there are only {n}. Part of the data may be missing.',
|
'Arquivo truncado: o cabeçalho indica {n} páginas, mas só há {n}. Parte dos dados pode estar faltando.': 'Truncated file: the header says {n} pages, but there are only {n}. Part of the data may be missing.',
|
||||||
|
|
||||||
|
// mensagens de erro e avisos do núcleo (valores variáveis usam {n})
|
||||||
|
'job {n} não existe': 'job {n} does not exist',
|
||||||
|
'a varredura não está pausada': 'the scan is not paused',
|
||||||
|
'A origem desta sessão não está disponível ({n}). Reconecte o disco ou escolha a origem para continuar.': 'The source of this session is unavailable ({n}). Reconnect the disk or choose the source to continue.',
|
||||||
|
'arquivo não encontrado': 'file not found',
|
||||||
|
'nenhum arquivo selecionado': 'no file selected',
|
||||||
|
'informe pelo menos um termo': 'enter at least one term',
|
||||||
|
'Espaço insuficiente no destino: os arquivos somam {n} e há {n} livres. Escolha outro disco ou recupere menos arquivos.': 'Not enough space at the destination: the files total {n} and {n} are free. Choose another disk or recover fewer files.',
|
||||||
|
'Sem permissão para ler a sessão {n}. Se ela foi criada como administrador (por exemplo em /root), abra o Guará como administrador ou copie o arquivo para a sua pasta.': 'Permission denied reading the session {n}. If it was created as administrator (for example in /root), open Guará as administrator or copy the file to your folder.',
|
||||||
|
'Não foi possível ler a sessão {n}: {n}': 'Could not read the session {n}: {n}',
|
||||||
|
'O arquivo {n} não é uma sessão do Guará válida (ou foi criado por uma versão incompatível): {n}': 'The file {n} is not a valid Guará session (or was created by an incompatible version): {n}',
|
||||||
|
'disco não encontrado': 'disk not found',
|
||||||
|
'Essa origem tem {n} bytes, mas a sessão foi feita sobre um disco de {n} bytes. Escolha o mesmo disco ou uma imagem dele.': 'This source has {n} bytes, but the session was made on a {n}-byte disk. Choose the same disk or an image of it.',
|
||||||
|
'O conteúdo inicial dessa origem é diferente do disco da sessão (outra tabela de partições ou outro disco). Se tem certeza, use a opção de usar assim mesmo.': 'The initial content of this source differs from the session disk (another partition table or another disk). If you are sure, use the \'use anyway\' option.',
|
||||||
|
'só sessões automáticas podem ser apagadas por aqui': 'only automatic sessions can be deleted here',
|
||||||
|
'parâmetro \'{n}\' ausente': 'missing parameter \'{n}\'',
|
||||||
|
'endereço não permitido': 'address not allowed',
|
||||||
|
'id ausente': 'missing id',
|
||||||
|
'método desconhecido: {n}': 'unknown method: {n}',
|
||||||
|
'A origem desta sessão não está disponível. Reconecte o disco ou escolha a origem para pré-visualizar e recuperar.': 'The source of this session is unavailable. Reconnect the disk or choose the source to preview and recover.',
|
||||||
|
'Sem permissão para ler {n}. Discos exigem privilégios de administrador: no Linux/macOS rode com sudo; no Windows, execute como administrador.': 'Permission denied reading {n}. Disks need administrator privileges: on Linux/macOS run with sudo; on Windows, run as administrator.',
|
||||||
|
'Não foi possível abrir {n}: {n}': 'Could not open {n}: {n}',
|
||||||
|
'O destino {n} fica em {n} (montado a partir da origem {n}). Escolha outro disco para não sobrescrever dados recuperáveis.': 'The destination {n} is on {n} (mounted from the source {n}). Choose another disk so you do not overwrite recoverable data.',
|
||||||
|
'A origem {n} contém o sistema (/). Recupere para um disco externo ou use uma imagem.': 'The source {n} contains the system (/). Recover to an external disk or use an image.',
|
||||||
|
'leitura além do fim da origem': 'read beyond the end of the source',
|
||||||
|
'senha ou chave incorreta': 'wrong password or key',
|
||||||
|
'não suportado: {n}': 'not supported: {n}',
|
||||||
|
'cabeçalho inválido: {n}': 'invalid header: {n}',
|
||||||
|
'Não é um banco SQLite (cabeçalho ausente). Se o arquivo é um banco, ele pode estar criptografado (SQLCipher) ou ter o início sobrescrito.': 'Not an SQLite database (missing header). If the file is a database, it may be encrypted (SQLCipher) or have its beginning overwritten.',
|
||||||
|
'Tamanho de página inválido no cabeçalho do banco.': 'Invalid page size in the database header.',
|
||||||
|
'página {n} fora do arquivo (banco truncado)': 'page {n} outside the file (truncated database)',
|
||||||
|
'página {n} incompleta': 'page {n} incomplete',
|
||||||
|
'página 0 inválida': 'invalid page 0',
|
||||||
|
'tabela {n} não encontrada': 'table {n} not found',
|
||||||
|
'célula ilegível na página {n}': 'unreadable cell on page {n}',
|
||||||
|
'página {n} de tipo inesperado (0x{n})': 'page {n} of unexpected type (0x{n})',
|
||||||
|
'Arquivo grande demais para abrir direto. Recupere-o e abra pelo seu gerenciador de arquivos.': 'File too large to open directly. Recover it and open it with your file manager.',
|
||||||
|
'não foi possível criar {n}: {n} (o arquivo não pode existir)': 'could not create {n}: {n} (the file must not exist)',
|
||||||
|
'não foi possível ler o arquivo de chave {n}: {n}': 'could not read the key file {n}: {n}',
|
||||||
|
'chave do volume em hexadecimal inválida': 'invalid hexadecimal volume key',
|
||||||
|
'não foi possível descobrir os parâmetros do RAID (nenhuma combinação produziu um sistema de arquivos coerente)': 'could not discover the RAID parameters (no combination produced a coherent file system)',
|
||||||
|
'não é uma imagem EWF (E01/Ex01)': 'not an EWF (E01/Ex01) image',
|
||||||
|
'imagem EWF sem tabela de chunks': 'EWF image without a chunk table',
|
||||||
|
'tabela EWF inconsistente': 'inconsistent EWF table',
|
||||||
|
'cabeçalho de volume EWF ausente': 'missing EWF volume header',
|
||||||
|
'segmento Ex01 ausente: sequência de chunks com lacuna': 'missing Ex01 segment: chunk sequence has a gap',
|
||||||
|
'tamanho de chunk EWF inválido': 'invalid EWF chunk size',
|
||||||
|
'imagem grande demais para o formato E01': 'image too large for the E01 format',
|
||||||
|
'não foi possível criar {n}: {n}': 'could not create {n}: {n}',
|
||||||
|
'AFF4 inválido (ZIP): {n}': 'invalid AFF4 (ZIP): {n}',
|
||||||
|
'AFF4: entrada sem início de dados: {n}': 'AFF4: entry without a data start: {n}',
|
||||||
|
'AFF4 sem information.turtle': 'AFF4 without information.turtle',
|
||||||
|
'AFF4 sem objeto de imagem (aff4:Image com aff4:dataStream)': 'AFF4 without an image object (aff4:Image with aff4:dataStream)',
|
||||||
|
'AFF4: fluxo de dados da imagem não encontrado': 'AFF4: image data stream not found',
|
||||||
|
'AFF4: fluxo simbólico inválido': 'AFF4: invalid symbolic stream',
|
||||||
|
'AFF4: fluxo {n} não descrito': 'AFF4: stream {n} not described',
|
||||||
|
'AFF4: entrada {n} ausente': 'AFF4: entry {n} missing',
|
||||||
|
'AFF4/ZIP: {n}': 'AFF4/ZIP: {n}',
|
||||||
|
'cifra {n} (apenas AES)': 'cipher {n} (only AES)',
|
||||||
|
'chave inválida para {n}': 'invalid key for {n}',
|
||||||
|
'IV {n}': 'IV {n}',
|
||||||
|
'{n} arquivos · {n} · {n}': '{n} files · {n} · {n}',
|
||||||
|
'Discos brutos exigem administrador. Rode': 'Raw disks require administrator. Run',
|
||||||
|
'(Linux/macOS) ou abra o Guará como administrador (Windows). Você ainda pode abrir um arquivo de imagem pelo menu.': '(Linux/macOS) or open Guará as administrator (Windows). You can still open an image file from the menu.',
|
||||||
|
'(Linux/macOS) ou como administrador (Windows).': '(Linux/macOS) or as administrator (Windows).',
|
||||||
|
'cifrada ({n}) · só detecção': 'encrypted ({n}) · detection only',
|
||||||
|
'Onde salvar o arquivo de imagem? Use': 'Where to save the image file? Use',
|
||||||
|
'outro disco': 'another disk',
|
||||||
|
'com espaço suficiente e um nome que ainda não exista.': 'with enough space and a name that does not exist yet.',
|
||||||
|
'Criar imagem': 'Create image',
|
||||||
|
'{n} trecho(s) ilegível(is) foram preenchidos com zeros. Veja o mapa de erros ao lado da imagem.': '{n} unreadable chunk(s) were filled with zeros. See the error map next to the image.',
|
||||||
|
'{n}. Escolha o disco que voltou ou uma imagem dele.': '{n}. Choose the disk that came back or an image of it.',
|
||||||
|
'Escolha uma pasta em': 'Choose a folder on',
|
||||||
|
'(todos desta vista)': '(all in this view)',
|
||||||
|
'Recuperar {n}': 'Recover {n}',
|
||||||
|
'Dimensões': 'Dimensions',
|
||||||
|
'{n} · {n}{n} · confiança {n} · {n} de {n}': '{n} · {n}{n} · confidence {n} · {n} of {n}',
|
||||||
|
'e mais {n}…': 'and {n} more…',
|
||||||
|
'abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'opens in your player: the embedded Linux browser does not include the codecs and would freeze the window.',
|
||||||
};
|
};
|
||||||
@@ -7,6 +7,7 @@ import { EN } from './i18n-en.js';
|
|||||||
|
|
||||||
let lang = 'pt';
|
let lang = 'pt';
|
||||||
export const isEn = () => lang === 'en';
|
export const isEn = () => lang === 'en';
|
||||||
|
export const currentLang = () => lang;
|
||||||
export const LOC = () => (lang === 'en' ? 'en-US' : 'pt-BR');
|
export const LOC = () => (lang === 'en' ? 'en-US' : 'pt-BR');
|
||||||
|
|
||||||
export function resolveLang(pref) {
|
export function resolveLang(pref) {
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ rust-version.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sha2 = "0.10"
|
sha2 = "0.11"
|
||||||
sha1 = "0.10"
|
sha1 = "0.11"
|
||||||
md-5 = "0.10"
|
md-5 = "0.11"
|
||||||
jpeg-decoder = { version = "0.3", default-features = false }
|
jpeg-decoder = { version = "0.3", default-features = false }
|
||||||
jpeg-encoder = "0.6"
|
jpeg-encoder = "0.6"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "gif", "bmp", "webp"] }
|
image = { version = "0.25", default-features = false, features = ["png", "gif", "bmp", "webp"] }
|
||||||
@@ -24,11 +24,11 @@ zip = { version = "9", default-features = false, features = ["deflate"] }
|
|||||||
snap = "1"
|
snap = "1"
|
||||||
ruzstd = "0.9"
|
ruzstd = "0.9"
|
||||||
lz4_flex = { version = "0.11", default-features = false, features = ["safe-decode", "safe-encode"] }
|
lz4_flex = { version = "0.11", default-features = false, features = ["safe-decode", "safe-encode"] }
|
||||||
aes = "0.8"
|
aes = "0.9"
|
||||||
hmac = "0.12"
|
hmac = "0.13"
|
||||||
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
|
pbkdf2 = { version = "0.13", default-features = false, features = ["hmac"] }
|
||||||
ripemd = "0.1"
|
ripemd = "0.2"
|
||||||
argon2 = "0.5"
|
argon2 = "0.6"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ pub mod filevault;
|
|||||||
pub mod luks;
|
pub mod luks;
|
||||||
|
|
||||||
use crate::source::Source;
|
use crate::source::Source;
|
||||||
use aes::cipher::generic_array::GenericArray;
|
use aes::cipher::{Block, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
|
||||||
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
|
|
||||||
use aes::{Aes128, Aes192, Aes256};
|
use aes::{Aes128, Aes192, Aes256};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -33,7 +32,7 @@ impl AesAny {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn encrypt(&self, b: &mut [u8; 16]) {
|
pub fn encrypt(&self, b: &mut [u8; 16]) {
|
||||||
let g = GenericArray::from_mut_slice(b);
|
let g: &mut Block<Aes128> = (&mut b[..]).try_into().expect("bloco AES de 16 bytes");
|
||||||
match self {
|
match self {
|
||||||
AesAny::A128(c) => c.encrypt_block(g),
|
AesAny::A128(c) => c.encrypt_block(g),
|
||||||
AesAny::A192(c) => c.encrypt_block(g),
|
AesAny::A192(c) => c.encrypt_block(g),
|
||||||
@@ -41,7 +40,7 @@ impl AesAny {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn decrypt(&self, b: &mut [u8; 16]) {
|
pub fn decrypt(&self, b: &mut [u8; 16]) {
|
||||||
let g = GenericArray::from_mut_slice(b);
|
let g: &mut Block<Aes128> = (&mut b[..]).try_into().expect("bloco AES de 16 bytes");
|
||||||
match self {
|
match self {
|
||||||
AesAny::A128(c) => c.decrypt_block(g),
|
AesAny::A128(c) => c.decrypt_block(g),
|
||||||
AesAny::A192(c) => c.decrypt_block(g),
|
AesAny::A192(c) => c.decrypt_block(g),
|
||||||
|
|||||||
@@ -444,11 +444,11 @@ fn exif_epoch(s: &str) -> Option<i64> {
|
|||||||
Some(crate::fs::days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se)
|
Some(crate::fs::days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// "2023:07:14 18:22:05" -> "14/07/2023 18:22:05"
|
/// "2023:07:14 18:22:05" -> "2023-07-14T18:22:05" (a interface formata por idioma)
|
||||||
fn exif_date(s: &str) -> String {
|
fn exif_date(s: &str) -> String {
|
||||||
let b = s.as_bytes();
|
let b = s.as_bytes();
|
||||||
if b.len() >= 19 && b[4] == b':' && b[7] == b':' {
|
if b.len() >= 19 && b[4] == b':' && b[7] == b':' {
|
||||||
format!("{}/{}/{} {}", &s[8..10], &s[5..7], &s[0..4], &s[11..19])
|
format!("{}-{}-{}T{}", &s[0..4], &s[5..7], &s[8..10], &s[11..19])
|
||||||
} else {
|
} else {
|
||||||
s.to_string()
|
s.to_string()
|
||||||
}
|
}
|
||||||
@@ -613,17 +613,17 @@ fn pdf_text(b: &[u8]) -> String {
|
|||||||
b.iter().map(|&c| c as char).collect()
|
b.iter().map(|&c| c as char).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// "D:20230714182205-03'00'" -> "14/07/2023 18:22:05"
|
/// "D:20230714182205-03'00'" -> "2023-07-14T18:22:05" (a interface formata por idioma)
|
||||||
fn pdf_date(s: &str) -> String {
|
fn pdf_date(s: &str) -> String {
|
||||||
let t = s.trim_start_matches("D:");
|
let t = s.trim_start_matches("D:");
|
||||||
let digits: String = t.chars().take_while(|c| c.is_ascii_digit()).collect();
|
let digits: String = t.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||||
if digits.len() >= 8 {
|
if digits.len() >= 8 {
|
||||||
let g = |a: usize, b: usize| digits.get(a..b).unwrap_or("00");
|
let g = |a: usize, b: usize| digits.get(a..b).unwrap_or("00");
|
||||||
return format!(
|
return format!(
|
||||||
"{}/{}/{} {}:{}:{}",
|
"{}-{}-{}T{}:{}:{}",
|
||||||
g(6, 8),
|
|
||||||
g(4, 6),
|
|
||||||
g(0, 4),
|
g(0, 4),
|
||||||
|
g(4, 6),
|
||||||
|
g(6, 8),
|
||||||
g(8, 10),
|
g(8, 10),
|
||||||
g(10, 12),
|
g(10, 12),
|
||||||
g(12, 14)
|
g(12, 14)
|
||||||
@@ -733,21 +733,18 @@ fn unescape(s: &str) -> String {
|
|||||||
.replace("&", "&")
|
.replace("&", "&")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// "2023-07-14T18:22:05Z" -> "14/07/2023 18:22:05 (UTC)"
|
/// Datas ISO 8601 ficam como estão; a interface as formata no idioma escolhido.
|
||||||
fn iso_date(s: &str) -> String {
|
fn iso_date(s: &str) -> String {
|
||||||
let b = s.as_bytes();
|
let b = s.as_bytes();
|
||||||
if b.len() >= 19 && b[4] == b'-' && b[10] == b'T' {
|
if b.len() >= 19 && b[4] == b'-' && b[10] == b'T' {
|
||||||
let utc = if s.ends_with('Z') { " (UTC)" } else { "" };
|
let mut out = s[..19].to_string();
|
||||||
return format!(
|
if s.len() == 20 && s.ends_with('Z') {
|
||||||
"{}/{}/{} {}{}",
|
out.push('Z');
|
||||||
&s[8..10],
|
}
|
||||||
&s[5..7],
|
out
|
||||||
&s[0..4],
|
} else {
|
||||||
&s[11..19],
|
s.to_string()
|
||||||
utc
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
s.to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ooxml(fr: &Fr, m: &mut Meta) {
|
fn ooxml(fr: &Fr, m: &mut Meta) {
|
||||||
@@ -890,7 +887,7 @@ fn moov(d: &[u8], m: &mut Meta) {
|
|||||||
if created > EPOCH_1904 {
|
if created > EPOCH_1904 {
|
||||||
m.add(
|
m.add(
|
||||||
"Gravado em (metadado)",
|
"Gravado em (metadado)",
|
||||||
fmt_unix((created - EPOCH_1904) as i64),
|
iso_unix((created - EPOCH_1904) as i64),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ts > 0 && dur > 0 {
|
if ts > 0 && dur > 0 {
|
||||||
@@ -919,6 +916,19 @@ fn moov(d: &[u8], m: &mut Meta) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Instante em ISO 8601 UTC ("2023-07-14T18:22:05Z"), para a interface formatar.
|
||||||
|
pub fn iso_unix(t: i64) -> String {
|
||||||
|
let days = t.div_euclid(86400);
|
||||||
|
let rem = t.rem_euclid(86400);
|
||||||
|
let (y, mo, d) = civil_from_days(days);
|
||||||
|
format!(
|
||||||
|
"{y:04}-{mo:02}-{d:02}T{:02}:{:02}:{:02}Z",
|
||||||
|
rem / 3600,
|
||||||
|
rem % 3600 / 60,
|
||||||
|
rem % 60
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fmt_unix(t: i64) -> String {
|
pub fn fmt_unix(t: i64) -> String {
|
||||||
let days = t.div_euclid(86400);
|
let days = t.div_euclid(86400);
|
||||||
let rem = t.rem_euclid(86400);
|
let rem = t.rem_euclid(86400);
|
||||||
|
|||||||
@@ -3,9 +3,59 @@
|
|||||||
|
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
/// Avisos que o núcleo envia ao sistema operacional. O texto é montado no
|
||||||
|
/// idioma efetivo da interface (`settings::ui_lang`), porque notificações não
|
||||||
|
/// passam pelo dicionário do DOM.
|
||||||
|
pub enum Notice {
|
||||||
|
ScanDone { count: usize, target: String },
|
||||||
|
RecoverDone { count: usize, destination: String },
|
||||||
|
DiskLost,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Notice {
|
||||||
|
fn text(&self, lang: &str) -> (String, String) {
|
||||||
|
let en = lang == "en";
|
||||||
|
match self {
|
||||||
|
Notice::ScanDone { count, target } => (
|
||||||
|
"Guará: scan complete".into(),
|
||||||
|
if en {
|
||||||
|
format!("{count} file(s) found in {target}.")
|
||||||
|
} else {
|
||||||
|
format!("{count} arquivo(s) encontrados em {target}.")
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Notice::RecoverDone { count, destination } => (
|
||||||
|
if en {
|
||||||
|
"Guará: recovery complete".into()
|
||||||
|
} else {
|
||||||
|
"Guará: recuperação concluída".into()
|
||||||
|
},
|
||||||
|
if en {
|
||||||
|
format!("{count} file(s) saved to {destination}")
|
||||||
|
} else {
|
||||||
|
format!("{count} arquivo(s) salvos em {destination}")
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Notice::DiskLost => (
|
||||||
|
if en {
|
||||||
|
"Guará: disk disconnected?".into()
|
||||||
|
} else {
|
||||||
|
"Guará: disco desconectado?".into()
|
||||||
|
},
|
||||||
|
if en {
|
||||||
|
"The scan was paused without losing anything. Reconnect the disk and continue."
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
"A varredura foi pausada sem perder nada. Reconecte o disco e continue.".into()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Envia uma notificação sem bloquear. Falhas são ignoradas (é só um aviso).
|
/// Envia uma notificação sem bloquear. Falhas são ignoradas (é só um aviso).
|
||||||
pub fn send(title: &str, body: &str) {
|
pub fn send(notice: Notice) {
|
||||||
let (title, body) = (title.to_string(), body.to_string());
|
let (title, body) = notice.text(&crate::settings::ui_lang());
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
if let Some(mut c) = command(&title, &body) {
|
if let Some(mut c) = command(&title, &body) {
|
||||||
if let Ok(mut child) = c
|
if let Ok(mut child) = c
|
||||||
|
|||||||
@@ -531,14 +531,10 @@ impl Engine {
|
|||||||
&& s.started.elapsed().as_secs() >= 8
|
&& s.started.elapsed().as_secs() >= 8
|
||||||
&& crate::settings::load().notify
|
&& crate::settings::load().notify
|
||||||
{
|
{
|
||||||
crate::notify::send(
|
crate::notify::send(crate::notify::Notice::RecoverDone {
|
||||||
"Guará: recuperação concluída",
|
count: rep.recovered.len(),
|
||||||
&format!(
|
destination: rep.destination.clone(),
|
||||||
"{} arquivo(s) salvos em {}",
|
});
|
||||||
rep.recovered.len(),
|
|
||||||
rep.destination
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
s.status = if rep.cancelled {
|
s.status = if rep.cancelled {
|
||||||
Status::Stopped
|
Status::Stopped
|
||||||
@@ -1300,14 +1296,10 @@ fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
|
|||||||
s.status = Status::Done;
|
s.status = Status::Done;
|
||||||
s.checkpoint = None;
|
s.checkpoint = None;
|
||||||
if s.started.elapsed().as_secs() >= 20 && crate::settings::load().notify {
|
if s.started.elapsed().as_secs() >= 20 && crate::settings::load().notify {
|
||||||
crate::notify::send(
|
crate::notify::send(crate::notify::Notice::ScanDone {
|
||||||
"Guará: varredura concluída",
|
count: s.found.len(),
|
||||||
&format!(
|
target: job.target.clone(),
|
||||||
"{} arquivo(s) encontrados em {}.",
|
});
|
||||||
s.found.len(),
|
|
||||||
job.target
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Outcome::Interrupted(cp) => {
|
Outcome::Interrupted(cp) => {
|
||||||
@@ -1317,7 +1309,7 @@ fn spawn_scan(job: Arc<Job>, cp: Option<Checkpoint>) {
|
|||||||
s.status = Status::Paused;
|
s.status = Status::Paused;
|
||||||
if cp.io_lost {
|
if cp.io_lost {
|
||||||
if crate::settings::load().notify {
|
if crate::settings::load().notify {
|
||||||
crate::notify::send("Guará: disco desconectado?", "A varredura foi pausada sem perder nada. Reconecte o disco e continue.");
|
crate::notify::send(crate::notify::Notice::DiskLost);
|
||||||
}
|
}
|
||||||
s.warnings.push("A leitura do dispositivo falhou repetidamente: ele provavelmente foi desconectado. A varredura foi pausada sem perder nada. Reconecte o disco e clique em Continuar.".into());
|
s.warnings.push("A leitura do dispositivo falhou repetidamente: ele provavelmente foi desconectado. A varredura foi pausada sem perder nada. Reconecte o disco e clique em Continuar.".into());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ pub struct Settings {
|
|||||||
/// Idioma da interface: "auto", "pt" ou "en".
|
/// Idioma da interface: "auto", "pt" ou "en".
|
||||||
#[serde(default = "auto")]
|
#[serde(default = "auto")]
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
/// Idioma que a interface resolveu de fato ("pt" ou "en"), para as
|
||||||
|
/// notificações do sistema e a saída do terminal acompanharem a tela.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ui_lang: Option<String>,
|
||||||
/// Notificação do sistema quando uma tarefa longa termina.
|
/// Notificação do sistema quando uma tarefa longa termina.
|
||||||
#[serde(default = "yes")]
|
#[serde(default = "yes")]
|
||||||
pub notify: bool,
|
pub notify: bool,
|
||||||
@@ -34,6 +38,7 @@ impl Default for Settings {
|
|||||||
Self {
|
Self {
|
||||||
theme: auto(),
|
theme: auto(),
|
||||||
lang: auto(),
|
lang: auto(),
|
||||||
|
ui_lang: None,
|
||||||
notify: true,
|
notify: true,
|
||||||
maps: false,
|
maps: false,
|
||||||
}
|
}
|
||||||
@@ -101,6 +106,30 @@ pub fn load() -> Settings {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Idioma efetivo para textos gerados fora da interface (notificações e CLI).
|
||||||
|
/// Usa o que a interface informou; sem isso, a preferência e o ambiente.
|
||||||
|
pub fn ui_lang() -> String {
|
||||||
|
let s = load();
|
||||||
|
if let Some(l) = s.ui_lang.as_deref() {
|
||||||
|
if l == "pt" || l == "en" {
|
||||||
|
return l.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.lang == "pt" || s.lang == "en" {
|
||||||
|
return s.lang;
|
||||||
|
}
|
||||||
|
let env = std::env::var("LC_ALL")
|
||||||
|
.or_else(|_| std::env::var("LC_MESSAGES"))
|
||||||
|
.or_else(|_| std::env::var("LANG"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
if env.starts_with("pt") {
|
||||||
|
"pt".into()
|
||||||
|
} else {
|
||||||
|
"en".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save(s: &Settings) -> Result<()> {
|
pub fn save(s: &Settings) -> Result<()> {
|
||||||
crate::owner::write_owned(&file(), serde_json::to_vec_pretty(s)?)?;
|
crate::owner::write_owned(&file(), serde_json::to_vec_pretty(s)?)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ fn configuracoes_antigas_ganham_os_valores_padrao_novos() {
|
|||||||
settings::save(&settings::Settings {
|
settings::save(&settings::Settings {
|
||||||
theme: "light".into(),
|
theme: "light".into(),
|
||||||
lang: "en".into(),
|
lang: "en".into(),
|
||||||
|
ui_lang: Some("en".into()),
|
||||||
notify: false,
|
notify: false,
|
||||||
maps: true,
|
maps: true,
|
||||||
})
|
})
|
||||||
@@ -167,7 +168,10 @@ fn notificacao_chama_o_notify_send() {
|
|||||||
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
|
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
let old = std::env::var("PATH").unwrap_or_default();
|
let old = std::env::var("PATH").unwrap_or_default();
|
||||||
std::env::set_var("PATH", format!("{}:{old}", bin.path().display()));
|
std::env::set_var("PATH", format!("{}:{old}", bin.path().display()));
|
||||||
notify::send("Guará: varredura concluída", "7 arquivos");
|
notify::send(notify::Notice::ScanDone {
|
||||||
|
count: 7,
|
||||||
|
target: "disco".into(),
|
||||||
|
});
|
||||||
for _ in 0..100 {
|
for _ in 0..100 {
|
||||||
if marker.exists() && !std::fs::read(&marker).unwrap().is_empty() {
|
if marker.exists() && !std::fs::read(&marker).unwrap().is_empty() {
|
||||||
break;
|
break;
|
||||||
@@ -176,8 +180,10 @@ fn notificacao_chama_o_notify_send() {
|
|||||||
}
|
}
|
||||||
std::env::set_var("PATH", old);
|
std::env::set_var("PATH", old);
|
||||||
let got = String::from_utf8(std::fs::read(&marker).unwrap()).unwrap();
|
let got = String::from_utf8(std::fs::read(&marker).unwrap()).unwrap();
|
||||||
|
assert!(got.contains("Guará: "), "{got}");
|
||||||
assert!(
|
assert!(
|
||||||
got.contains("Guará: varredura concluída") && got.contains("7 arquivos"),
|
got.contains("7 arquivo(s) encontrados em disco.")
|
||||||
|
|| got.contains("7 file(s) found in disco."),
|
||||||
"{got}"
|
"{got}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ fn exif_camera_data_e_gps() {
|
|||||||
assert!(m
|
assert!(m
|
||||||
.fields
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(k, v)| k == "Foto tirada em" && v == "14/07/2023 18:22:05"));
|
.any(|(k, v)| k == "Foto tirada em" && v == "2023-07-14T18:22:05"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -199,7 +199,7 @@ fn metadados_de_pdf() {
|
|||||||
assert_eq!(get("Autor").as_deref(), Some("Maria (Perita) Silva"));
|
assert_eq!(get("Autor").as_deref(), Some("Maria (Perita) Silva"));
|
||||||
assert_eq!(get("Criado com").as_deref(), Some("LibreOffice Writer"));
|
assert_eq!(get("Criado com").as_deref(), Some("LibreOffice Writer"));
|
||||||
assert_eq!(get("Gerado por").as_deref(), Some("PDF12"));
|
assert_eq!(get("Gerado por").as_deref(), Some("PDF12"));
|
||||||
assert_eq!(get("Criado em").as_deref(), Some("14/07/2023 18:22:05"));
|
assert_eq!(get("Criado em").as_deref(), Some("2023-07-14T18:22:05"));
|
||||||
assert_eq!(get("Páginas (aprox.)").as_deref(), Some("12"));
|
assert_eq!(get("Páginas (aprox.)").as_deref(), Some("12"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,10 +279,7 @@ fn metadados_de_documentos_office() {
|
|||||||
);
|
);
|
||||||
assert_eq!(get("Autor").as_deref(), Some("Ana Souza"));
|
assert_eq!(get("Autor").as_deref(), Some("Ana Souza"));
|
||||||
assert_eq!(get("Última modificação por").as_deref(), Some("Bruno Lima"));
|
assert_eq!(get("Última modificação por").as_deref(), Some("Bruno Lima"));
|
||||||
assert_eq!(
|
assert_eq!(get("Criado em").as_deref(), Some("2023-07-14T18:22:05Z"));
|
||||||
get("Criado em").as_deref(),
|
|
||||||
Some("14/07/2023 18:22:05 (UTC)")
|
|
||||||
);
|
|
||||||
assert_eq!(get("Aplicativo").as_deref(), Some("Microsoft Office Word"));
|
assert_eq!(get("Aplicativo").as_deref(), Some("Microsoft Office Word"));
|
||||||
assert_eq!(get("Empresa").as_deref(), Some("ACME"));
|
assert_eq!(get("Empresa").as_deref(), Some("ACME"));
|
||||||
}
|
}
|
||||||
@@ -323,7 +320,7 @@ fn metadados_de_mp4() {
|
|||||||
assert_eq!(get("Duração").as_deref(), Some("0:02:05"));
|
assert_eq!(get("Duração").as_deref(), Some("0:02:05"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get("Gravado em (metadado)").as_deref(),
|
get("Gravado em (metadado)").as_deref(),
|
||||||
Some("14/07/2023 18:22:05 (UTC)")
|
Some("2023-07-14T18:22:05Z")
|
||||||
);
|
);
|
||||||
assert_eq!(get("Dimensões").as_deref(), Some("1920 × 1080 px"));
|
assert_eq!(get("Dimensões").as_deref(), Some("1920 × 1080 px"));
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -52,7 +52,8 @@ docker run --rm \
|
|||||||
|
|
||||||
- The destination must be on **another disk** than `/dev/sdb`.
|
- The destination must be on **another disk** than `/dev/sdb`.
|
||||||
- Reading a raw device needs root inside the container, so do not pass `--user` here.
|
- Reading a raw device needs root inside the container, so do not pass `--user` here.
|
||||||
- To read SMART, `--device` is not enough on some systems; `--cap-add=SYS_RAWIO` is the usual extra.
|
- Verified locally with a loop device (Linux): `inspect`, `scan` and `recover` read through `--device=...:r`, and a write attempt (`dd of=/dev/loop0`) fails with `Operation not permitted`. Loop devices are hidden from `guara devices`, like other virtual devices, so pass the path explicitly; a real USB disk does appear in the list.
|
||||||
|
- SMART depends on the host, the controller and the device. `--cap-add=SYS_RAWIO` is the usual addition when `--device` alone is not enough, but this was **not** verified with a loop device, which has no SMART data.
|
||||||
- Avoid `--privileged`: it gives the container access to every device.
|
- Avoid `--privileged`: it gives the container access to every device.
|
||||||
|
|
||||||
## Other commands
|
## Other commands
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ docker run --rm \
|
|||||||
|
|
||||||
- O destino precisa estar em **outro disco** que não seja o `/dev/sdb`.
|
- O destino precisa estar em **outro disco** que não seja o `/dev/sdb`.
|
||||||
- Ler dispositivo bruto exige root dentro do container, então não use `--user` aqui.
|
- Ler dispositivo bruto exige root dentro do container, então não use `--user` aqui.
|
||||||
- Para ler o SMART, em alguns sistemas `--device` não basta; o complemento usual é `--cap-add=SYS_RAWIO`.
|
- Verificado localmente com um dispositivo de loop (Linux): `inspect`, `scan` e `recover` leem através de `--device=...:r`, e uma tentativa de escrita (`dd of=/dev/loop0`) falha com `Operation not permitted`. Dispositivos de loop ficam ocultos no `guara devices`, como outros dispositivos virtuais, então informe o caminho explicitamente; um pendrive real aparece na lista.
|
||||||
|
- O SMART depende do hospedeiro, do controlador e do dispositivo. `--cap-add=SYS_RAWIO` é o complemento usual quando só `--device` não basta, mas isso **não** foi verificado com um dispositivo de loop, que não tem dados SMART.
|
||||||
- Evite `--privileged`: ele dá ao container acesso a todos os dispositivos.
|
- Evite `--privileged`: ele dá ao container acesso a todos os dispositivos.
|
||||||
|
|
||||||
## Outros comandos
|
## Outros comandos
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[English](../support.md) · **Português**
|
[English](../support.md) · **Português**
|
||||||
|
|
||||||
Matriz do que existe hoje (versão 1.0.1). Para o que vem a seguir, veja o [ROADMAP](../../ROADMAP.pt-BR.md).
|
Matriz do que existe hoje (versão 1.1.0). Para o que vem a seguir, veja o [ROADMAP](../../ROADMAP.pt-BR.md).
|
||||||
|
|
||||||
## Tabelas de partição
|
## Tabelas de partição
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[English](support.md) · [Português](pt-BR/suporte.md)
|
[English](support.md) · [Português](pt-BR/suporte.md)
|
||||||
|
|
||||||
Matrix of what exists today (version 1.0.1). For what comes next, see the [ROADMAP](../ROADMAP.md).
|
Matrix of what exists today (version 1.1.0). For what comes next, see the [ROADMAP](../ROADMAP.md).
|
||||||
|
|
||||||
## Partition tables
|
## Partition tables
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Confere se o dicionário inglês cobre os textos da interface.
|
||||||
|
//
|
||||||
|
// A varredura é estática e sem dependências: lê os arquivos da interface
|
||||||
|
// (JS e HTML), os rótulos que o núcleo em Rust envia para a tela (metadados,
|
||||||
|
// tipos de arquivo e saúde do disco) e falha quando encontra uma string que
|
||||||
|
// parece português e não tem tradução. Também recusa chaves duplicadas no
|
||||||
|
// dicionário (num objeto JS a última vence em silêncio).
|
||||||
|
//
|
||||||
|
// Uso: node scripts/check-ui-i18n.mjs
|
||||||
|
|
||||||
|
import { readFileSync, readdirSync } from 'node:fs';
|
||||||
|
import { join, relative } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
||||||
|
const rel = (p) => relative(root, p).replaceAll('\\', '/');
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- dicionário
|
||||||
|
|
||||||
|
const dictPath = join(root, 'apps/gui/ui/i18n-en.js');
|
||||||
|
const dictSource = readFileSync(dictPath, 'utf8');
|
||||||
|
|
||||||
|
const keyRe = /^\s*(['"])((?:(?!\1).)*)\1\s*:/gm;
|
||||||
|
const seen = new Map();
|
||||||
|
const duplicates = [];
|
||||||
|
let m;
|
||||||
|
while ((m = keyRe.exec(dictSource))) {
|
||||||
|
const key = m[2];
|
||||||
|
const line = dictSource.slice(0, m.index).split('\n').length;
|
||||||
|
if (seen.has(key)) duplicates.push({ key, first: seen.get(key), again: line });
|
||||||
|
else seen.set(key, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicates.length) {
|
||||||
|
console.error('Chaves duplicadas em apps/gui/ui/i18n-en.js:');
|
||||||
|
for (const d of duplicates) console.error(` ${JSON.stringify(d.key)} (linhas ${d.first} e ${d.again})`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { EN } = await import(new URL('../apps/gui/ui/i18n-en.js', import.meta.url).href);
|
||||||
|
|
||||||
|
const exact = new Set(Object.keys(EN).filter((k) => !k.includes('{n}')));
|
||||||
|
const patterns = Object.keys(EN)
|
||||||
|
.filter((k) => k.includes('{n}'))
|
||||||
|
.map((k) => {
|
||||||
|
const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
|
return new RegExp('^' + parts.join('(.+?)') + '$', 's');
|
||||||
|
});
|
||||||
|
|
||||||
|
function translated(text) {
|
||||||
|
if (exact.has(text)) return true;
|
||||||
|
return patterns.some((re) => re.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- heurística
|
||||||
|
|
||||||
|
const ACCENT = /[áàâãäéèêëíìîïóòôõöúùûüçÁÀÂÃÄÉÈÊËÍÌÎÏÓÒÔÕÖÚÙÛÜÇ]/;
|
||||||
|
const WORDS = [
|
||||||
|
'arquivo', 'arquivos', 'pasta', 'pastas', 'selecionado', 'selecionados', 'selecionada',
|
||||||
|
'selecionadas', 'item', 'itens', 'abrir', 'abre', 'salvar', 'salvo', 'salvos', 'cancelar',
|
||||||
|
'fechar', 'buscar', 'procurar', 'procurando', 'disco', 'discos', 'senha', 'chave', 'origem',
|
||||||
|
'destino', 'tamanho', 'nome', 'nenhum', 'nenhuma', 'todos', 'todas', 'cada', 'esta', 'este',
|
||||||
|
'essa', 'esse', 'para', 'com', 'sem', 'dos', 'das', 'por', 'que', 'não', 'sim', 'mais',
|
||||||
|
'menos', 'uma', 'uns', 'umas', 'aqui', 'onde', 'quando', 'como', 'qual', 'confiança',
|
||||||
|
'recuperar', 'recupera', 'recuperação', 'varredura', 'imagem', 'sessão', 'mês', 'dia',
|
||||||
|
'hora', 'hoje', 'ontem', 'modificado', 'criado', 'acessado', 'tirada', 'foto', 'fotos',
|
||||||
|
'documentos', 'vídeos', 'vídeo', 'áudio', 'compactados', 'banco', 'bancos', 'dados',
|
||||||
|
];
|
||||||
|
|
||||||
|
const BRAND = /Guará|Português|English|OpenStreetMap|BitLocker|FileVault|LUKS|NO\u00c7\u00d5ES|S\u00c3O/gi;
|
||||||
|
const isPortuguese = (raw) => {
|
||||||
|
const s = raw.replace(BRAND, ' ');
|
||||||
|
return ACCENT.test(s) || WORDS.some((w) => new RegExp(`(^|[^a-zà-ú])${w}([^a-zà-ú]|$)`, 'i').test(s));
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALLOW = new Set([
|
||||||
|
'Guará', 'Português', 'English', 'Auto', 'SMART', 'NVMe', 'OpenStreetMap', 'SHA-256',
|
||||||
|
'SHA-1', 'MD5', 'AES', 'LUKS', 'BitLocker', 'FileVault', 'APFS', 'HFS+', 'ext4', 'Btrfs',
|
||||||
|
'XFS', 'NTFS', 'FAT32', 'exFAT', 'ReFS', 'RAID', 'LVM', 'SQLite', 'CSV', 'HTML', 'PDF',
|
||||||
|
'JPEG', 'PNG', 'GIF', 'BMP', 'WebP', 'MP4', 'MOV', 'E01', 'Ex01', 'AFF4', 'UTF-8',
|
||||||
|
'UTF-16', 'USB', 'SSD', 'ATA', 'CRC', 'GPS', 'EXIF', 'TRIM', 'ZIP', 'TAR', 'OK',
|
||||||
|
]);
|
||||||
|
const ALLOW_PREFIX = ['http://', 'https://', 'data:', 'file:', 'guara:', '#', '.', '/'];
|
||||||
|
const ALLOW_EXACT = new Set([
|
||||||
|
'sem data', 'no date', 'n/a', '—', '-', '·',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function suspicious(text) {
|
||||||
|
const s = text.replace(/\s+/g, ' ').trim();
|
||||||
|
if (!s || s.length < 2) return false;
|
||||||
|
if (ALLOW.has(s) || ALLOW_EXACT.has(s)) return false;
|
||||||
|
if (ALLOW_PREFIX.some((p) => s.startsWith(p))) return false;
|
||||||
|
if (!isPortuguese(s)) return false;
|
||||||
|
// técnico: seletores, classes, extensões, expressões
|
||||||
|
if (/^[.#\w-]+(\s*[,>+~]\s*[.#\w-]+)+$/.test(s)) return false;
|
||||||
|
if (/^[a-z0-9_.:/@%{}$-]+$/i.test(s) && !ACCENT.test(s)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- extratores
|
||||||
|
|
||||||
|
function jsStrings(source, file, baseLine = 1) {
|
||||||
|
const out = [];
|
||||||
|
let i = 0;
|
||||||
|
let line = baseLine;
|
||||||
|
let prev = '';
|
||||||
|
const skipRegex = () => {
|
||||||
|
i++;
|
||||||
|
let inClass = false;
|
||||||
|
while (i < source.length) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (ch === '\\') { i += 2; continue; }
|
||||||
|
if (ch === '[') inClass = true;
|
||||||
|
else if (ch === ']') inClass = false;
|
||||||
|
else if (ch === '/' && !inClass) { i++; break; }
|
||||||
|
else if (ch === '\n') line++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (i < source.length && /[a-z]/i.test(source[i])) i++;
|
||||||
|
};
|
||||||
|
while (i < source.length) {
|
||||||
|
const c = source[i];
|
||||||
|
if (c === '\n') { line++; i++; prev = '\n'; continue; }
|
||||||
|
if (c === ' ' || c === '\t' || c === '\r') { i++; continue; }
|
||||||
|
if (c === '/' && source[i + 1] === '/') {
|
||||||
|
while (i < source.length && source[i] !== '\n') i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === '/' && source[i + 1] === '*') {
|
||||||
|
i += 2;
|
||||||
|
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) { if (source[i] === '\n') line++; i++; }
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === '/' && (/[=(:,[!&|?{};+\-*%^~<>]/.test(prev) || prev === 'return')) {
|
||||||
|
skipRegex();
|
||||||
|
prev = ')';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "'" || c === '"') {
|
||||||
|
const quote = c;
|
||||||
|
const startLine = line;
|
||||||
|
let value = '';
|
||||||
|
i++;
|
||||||
|
while (i < source.length) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (ch === '\\') {
|
||||||
|
const n = source[i + 1];
|
||||||
|
if (n === 'n') value += '\n';
|
||||||
|
else if (n === 't') value += '\t';
|
||||||
|
else if (n === 'r') value += '\r';
|
||||||
|
else if (n === '\n') line++;
|
||||||
|
else value += n ?? '';
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === quote) { i++; break; }
|
||||||
|
if (ch === '\n') line++;
|
||||||
|
value += ch;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
out.push({ text: value, file, line: startLine });
|
||||||
|
prev = ')';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === '`') {
|
||||||
|
const startLine = line;
|
||||||
|
let value = '';
|
||||||
|
i++;
|
||||||
|
while (i < source.length) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (ch === '\\') {
|
||||||
|
const n = source[i + 1];
|
||||||
|
if (n === 'n') value += '\n';
|
||||||
|
else if (n === 't') value += '\t';
|
||||||
|
else if (n === '\n') line++;
|
||||||
|
else value += n ?? '';
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '`') { i++; break; }
|
||||||
|
if (ch === '$' && source[i + 1] === '{') {
|
||||||
|
value += '{n}';
|
||||||
|
i += 2;
|
||||||
|
const exprStart = i;
|
||||||
|
const exprLine = line;
|
||||||
|
let depth = 1;
|
||||||
|
while (i < source.length && depth > 0) {
|
||||||
|
if (source[i] === '{') depth++;
|
||||||
|
else if (source[i] === '}') depth--;
|
||||||
|
else if (source[i] === '\n') line++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
out.push(...jsStrings(source.slice(exprStart, i - 1), file, exprLine));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '\n') line++;
|
||||||
|
value += ch;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
out.push({ text: value, file, line: startLine });
|
||||||
|
prev = ')';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
prev = c;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function htmlStrings(source, file, baseLine = 1) {
|
||||||
|
const out = [];
|
||||||
|
const lineAt = (idx) => baseLine + source.slice(0, idx).split('\n').length - 1;
|
||||||
|
// todo texto entre tags (inclusive começo e fim) — nós de texto reais
|
||||||
|
let last = 0;
|
||||||
|
for (const m of source.matchAll(/<[^>]*>/g)) {
|
||||||
|
out.push({ text: source.slice(last, m.index), file, line: lineAt(last) });
|
||||||
|
last = m.index + m[0].length;
|
||||||
|
}
|
||||||
|
out.push({ text: source.slice(last), file, line: lineAt(last) });
|
||||||
|
// atributos traduzíveis (com aspas simples ou duplas)
|
||||||
|
for (const m of source.matchAll(/\b(placeholder|title|aria-label|alt|data-empty)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
|
||||||
|
out.push({ text: m[2] ?? m[3], file, line: lineAt(m.index) });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rustLabels(source, file) {
|
||||||
|
const out = [];
|
||||||
|
const lineAt = (idx) => source.slice(0, idx).split('\n').length;
|
||||||
|
// m.add("Rótulo", ...) e m.add(\n "Rótulo", ...)
|
||||||
|
for (const m of source.matchAll(/\.add\(\s*"([^"]+)"/g)) out.push({ text: m[1], file, line: lineAt(m.index) });
|
||||||
|
// m.kind = "Tipo".into()
|
||||||
|
for (const m of source.matchAll(/\.kind\s*=\s*"([^"]+)"/g)) out.push({ text: m[1], file, line: lineAt(m.index) });
|
||||||
|
// rótulos de listas (saúde do disco, tipos de arquivo) dentro de arrays de tuplas
|
||||||
|
for (const m of source.matchAll(/\(\s*"([^"]{2,60})"\s*,\s*"(?:[^"]*)"\s*\)/g)) out.push({ text: m[1], file, line: lineAt(m.index) });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- coleta
|
||||||
|
|
||||||
|
const uiFiles = readdirSync(join(root, 'apps/gui/ui'))
|
||||||
|
.filter((f) => f.endsWith('.js') && f !== 'i18n-en.js')
|
||||||
|
.map((f) => join(root, 'apps/gui/ui', f));
|
||||||
|
uiFiles.push(join(root, 'apps/gui/ui/index.html'));
|
||||||
|
|
||||||
|
const rustFiles = [
|
||||||
|
'crates/guara-core/src/meta.rs',
|
||||||
|
'crates/guara-core/src/explorer.rs',
|
||||||
|
'crates/guara-core/src/types.rs',
|
||||||
|
'crates/guara-core/src/health.rs',
|
||||||
|
].map((f) => join(root, f));
|
||||||
|
|
||||||
|
const found = [];
|
||||||
|
for (const file of uiFiles) {
|
||||||
|
const source = readFileSync(file, 'utf8');
|
||||||
|
if (file.endsWith('.html')) found.push(...htmlStrings(source, file));
|
||||||
|
else {
|
||||||
|
for (const str of jsStrings(source, file)) {
|
||||||
|
if (/<[a-z!/][^>]*>/i.test(str.text)) found.push(...htmlStrings(str.text, file, str.line));
|
||||||
|
else found.push(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const file of rustFiles) found.push(...rustLabels(readFileSync(file, 'utf8'), file));
|
||||||
|
|
||||||
|
const missing = [];
|
||||||
|
const seenMissing = new Set();
|
||||||
|
for (const { text, file, line } of found) {
|
||||||
|
if (!suspicious(text)) continue;
|
||||||
|
const key = text.replace(/\s+/g, ' ').trim();
|
||||||
|
if (translated(key)) continue;
|
||||||
|
const id = `${key}\u0000${rel(file)}\u0000${line}`;
|
||||||
|
if (seenMissing.has(id)) continue;
|
||||||
|
seenMissing.add(id);
|
||||||
|
missing.push({ text: key, file: rel(file), line });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.length) {
|
||||||
|
console.error('Textos em português sem tradução em apps/gui/ui/i18n-en.js:\n');
|
||||||
|
for (const x of missing) console.error(` ${x.file}:${x.line} ${JSON.stringify(x.text)}`);
|
||||||
|
console.error(`\n${missing.length} texto(s) sem tradução.`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.exitCode) {
|
||||||
|
console.log(`i18n ok: ${found.length} textos verificados, ${Object.keys(EN).length} chaves no dicionário.`);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user